자바스크립트의 비동기 처리의 진실
제발 순서대로좀 실행되라!!
여는글
안녕하세요. 이번 포스팅에서는 자바스크립트의 비동기 처리에 대해 이야기해보겠습니다. 저는 주로 JAVA 개발을 하지만, 실무에서 자바스크립트를 사용할 때마다 코드 실행 순서가 예상과 다르게 동작하는 경우가 종종 발생합니다.
특히, 비동기 작업을 처리할 때 ‘내가 의도한 순서’대로 코드가 실행되지 않아 당황스러운 경험이 많았습니다. 오늘은 이 문제를 자바스크립트 자료구조 관점에서 풀어보고, 왜 이런 현상이 발생하는지 함께 알아보겠습니다.
싱글 스레드(Single Thread)
자바스크립트는 싱글 스레드 기반 언어입니다. 즉, 한 번에 하나의 작업만 처리할 수 있다는 뜻이죠.
자바스크립트는 비동기 작업을 지원하지만, 코드를 실행할 때는 하나의 작업이 끝나야 다음 작업을 처리할 수 있습니다. 많은 분들이 혼동할 수 있는데요. “자바스크립트는 비동기적으로 동작하지 않나?”
맞습니다. 자바스크립트는 비동기 작업을 지원하지만, 그 본질은 싱글 스레드입니다. 자바스크립트는 콜 스택과 이벤트 루프라는 특별한 메커니즘을 통해 비동기 작업을 처리하며, 이를 통해 동시에 여러 작업이 처리되는 것처럼 보이게 할 수 있습니다. 이를 더 자세히 설명하기 위해 콜 스택부터 시작해보겠습니다.
콜 스택(Call Stack)
콜 스택(Call Stack)은 Javascript에서 함수 호출을 관리하는 자료구조입니다. 스택의 원리(LIFO, Last In First Out)에 따라 함수가 호출될 때마다 콜 스택에 쌓이고, 실행이 끝나면 스택에서 제거됩니다. 즉, 동기적으로 실행되는 코드들은 콜 스택을 통해 순서대로 처리됩니다.
아래 예시코드를 살펴보면
function first() {
console.log('First function');
second();
}
function second() {
console.log('Second function');
third();
}
function third() {
console.log('Third function');
}
first();
- first() 함수가 호출되면, 콜 스택에 first가 쌓입니다.
- first 함수 내에서 second()가 호출되면, second가 콜 스택에 쌓입니다.
- second 함수 내에서 third()가 호출되면, third가 콜 스택에 쌓입니다.
- third 함수가 완료되면 스택에서 제거되고, 이후 second와 first도 차례로 스택에서 제거됩니다.
그림으로 살펴볼까요.
위 예시처럼 동기적인 코드는 위와 같이 콜 스택에 함수가 순차적으로 쌓이고 처리되며, 하나의 함수가 끝날 때까지 다음 작업이 대기하는 방식으로 작동합니다.
콜백 큐(Callback Queue)
비동기 코드는 이와 다르게 동작합니다. 자바스크립트에서 비동기 작업이 발생하면, 해당 작업이 끝난 후 그 작업의 콜백 함수는 콜백 큐에 저장됩니다. 그리고 콜 스택이 모두 비워지면, 이벤트 루프가 콜백 큐에 있는 콜백을 콜 스택에 올려 실행합니다.
즉, 비동기 함수는 완료 후에도 즉시 실행되지 않고, 콜 스택이 비워진 후에야 실행됩니다. 이 때문에 우리가 예상한 순서대로 실행되지 않는 일이 발생할 수 있습니다. 이제 이 개념을 더 구체적으로 살펴보기 위해 자바스크립트의 비동기 처리 방식인 then()
과 async/await
을 사용한 예시를 보겠습니다.
API 요청 전후 동기/비동기 처리
자바스크립트에서 API 요청은 비동기적으로 처리됩니다. 이 때문에 API 응답을 기다리기 전에 다음 코드가 실행되는 경우가 발생할 수 있습니다. 이를 방지하기 위해 자주 사용되는 것이 then()
과 async/await
패턴입니다.
// 1번 예시
function processData() {
fetch(`https://api.example.com/data/1`)
.then((response) => response.json())
.then((data) => {
processNestedData(data);
});
function processNestedData(data) {
fetch(`https://api.example.com/nested/${data.id}`)
.then((response) => response.json())
.then((nestedData) => {
console.log('Nested data:', nestedData);
});
}
}
processData();
위 코드는 fetch 함수를 사용해 데이터를 가져오는 비동기 처리 예시입니다. 이때, then()
의 내부 함수도 콜백 큐에 대기하다가, 콜 스택이 비워지면 실행됩니다. 따라서, then()
내부 또한, 비동기 코드와 동기 코드가 혼재되어 있을 때 순서를 신경 쓰지 않으면 의도한 동작과는 다르게 실행될 수 있습니다.
//2번 예시
async function processData() {
const response = await fetch(`https://api.example.com/data/1`);
const data = await response.json();
await processNestedData(data);
async function processNestedData(data) {
const nestedResponse = await fetch(`https://api.example.com/nested/${data.id}`);
const nestedData = await nestedResponse.json();
console.log('Nested data:', nestedData);
}
}
processData();
여기서 async/await
을 사용한 코드는 비동기 처리를 동기처럼 직관적으로 처리할 수 있습니다. await
는 해당 비동기 작업이 완료될 때까지 기다리기 때문에 코드의 실행 순서가 명확하게 보장됩니다.
정리
- 동기적 코드 즉시 콜 스택에 저장되고, 콜 스택 상에서 바로 실행됩니다.
- 비동기적 코드는 콜백 큐에서 대기 상태로 저장되어있다가 콜 스택이 모두 비워진 후에야 실행이 됩니다.
이처럼 자바스크립트에서 비동기 코드를 정확하게 다루기 위해서는 콜 스택과 콜백 큐에 대한 개념을 잘 이해하고 있어야 의도한대로 동작하는 코드를 작성할 수 있습니다.
또한, 내가 사용하는 함수가 비동기 처리되는 함수인지 동기 처리되는 함수인지 알고 사용해야, 코드가 의도한 대로 동작하지 않는 상황을 방지할 수 있습니다.