Introduce
자바스크립트에서 동기와 비동기를 이해하는 것은 다른 프로그래밍 언어보다 중요하다. 왜냐하면 자바스크립트는 메일 스레드 하나만을 사용하는 단일 스레드 방식으로 설계되어 있으며 메인 스레드에서 뷰가 렌더링되고 사용자와의 상호작용이 많기 때문이다. 브라우저에서 메인 스레드가 blocking되면 웹페이지가 멈추는 프리징 현상이 발생하기에 사용자 경험에 문제가 생길 수 있다.
따라서 이 글에서는 동기와 비동기의 차이를 알아보고, 브라우저에서 이 두 방식이 어떻게 동작하는지 살펴보고자 한다.
Preliminaries
Call stack
Call stack은 자바스크립트 엔진이 현재 실행 중인 함수와 그 함수들이 호출한 다른 함수들을 추적하는 자료 구조다. 이는 LIFO(Last In, First Out) 원칙에 따라 동작한다. 함수가 호출되면 Call stack의 맨 위에 push되고, 함수의 실행이 종료되면 Call stack에서 pop된다.
Callback queue
Callback queue는 비동기 작업의 Callback 함수들이 대기하는 곳이다. 비동기 작업이 스택에서 pop되면 해당 Callback 함수가 콜백 큐에 추가된다. Callback queue는 FIFO(First In, First Out) 원칙에 따라 동작한다. 즉, 먼저 들어온 Callback 함수가 먼저 실행된다.
Event Loop
Event loop는 Call stack과 Callback queue 사이에서 계속 돌아가며, Call stack이 비어 있는지 확인한다. Call stack이 비어 있으면 Callback queue에서 Callback 함수를 꺼내어 콜 스택에 푸시한다. 이를 통해 비동기 작업의 Callback함수가 실행된다.
Synchronous vs Asynchronous
동기 함수 (Synchronous)
자바스크립트에서 동기 함수는 호출된 순서대로 실행되며, 각 함수가 완료될 때까지 다음 함수는 실행되지 않는다. 즉, 하나의 함수가 완료되기 전까지 다른 코드의 실행이 block되어 기다려야 한다. 이렇게 동기 함수는 함수의 실행 순서가 보장된다. 따라서 콜 스택에 함수가 차례대로 push되고, 완료되면 pop된다.
예를 들어, 아래의 코드에서 함수 foo와 bar는 순차적으로 실행된다:
function foo() {
console.log('foo 실행 중');
console.log('foo 완료');
}
function bar() {
console.log('bar 실행 중');
console.log('bar 완료');
}
console.log('동기 함수 실행 시작');
foo();
bar();
console.log('동기 함수 실행 완료');
해당 함수의 출력을 보면 아래와 같음을 알 수 있다.
동기 함수 실행 시작
foo 실행 중
foo 완료
bar 실행 중
bar 완료
동기 함수 실행 완료
이처럼 동기 함수는 실행 순서가 보장되며, 각 함수가 완료될 때까지 다음 함수는 대기한다. 이러한 동작 방식은 코드의 흐름을 쉽게 이해할 수 있게 하지만, 시간이 오래 걸리는 작업이 있을 경우 전체 코드 실행이 지연될 수 있다. 예를 들어 만약 foo함수가 실행하는데 1분이 걸린다면, bar함수는 1분동안 기다려야 하는 문제가 생긴다. 이런 문제를 해결하기 위해 사용하는 것이 비동기 함수이다.
비동기 함수 (Asynchronous)
자바스크립트에서 비동기 프로그래밍은 오래 걸릴 수 있는 작업을 시작하면서도 다른 이벤트에 대한 응답성을 유지할 수 있게 해주는 기법이다. 이러한 비동기 함수는 작업이 완료될 때까지 기다릴 필요 없이, 작업이 완료되면 그 결과를 받을 수 있게 한다. 비동기 함수는 호출 즉시 다음 코드로 넘어가며, 작업이 완료될 때까지 다른 작업을 계속 수행할 수 있다. 이는 메인 스레드가 차단되지 않도록 하여 사용자 경험을 향상시킨다. 따라서 비동기 함수는 실행 순서가 보장되지 않는다. 즉, 호출된 순서와 상관없이 작업이 완료된 순서대로 결과를 받게 된다.
아래의 그림을 통해 비동기 함수의 동작 방식을 설명할 수 있다. 비동기 함수는 호출 즉시 Call Stack에서 제거되지만, 특정 작업이 완료되면 나중에 Callback 함수가 실행된다. 먼저 비동기 함수가 호출되면 실행순서는 다음과 같다.
- 그 함수는 Call Stack에 push되고 즉시 실행되고 Call stack에서 pop된다.
- 비동기 함수가 호출되면서, 그 작업은 웹 API 영역으로 이동된다.
- 작업이 완료되면, 해당 콜백 함수는 Event Queue 또는 Callback Queue에 등록된다.
- Event Loop는 지속적으로 Call stack과 Event Queue를 모니터링한다.
- Call stack이 비어 있는 상태가 되면, Event Loop는 Callback Queue에서 대기 중인 Callback 함수를 꺼내와 Call Stack에 push한다.
- Call Stack에 push된 Callback 함수는 실행되고, 완료되면 Call Stack에서 pop된다.
아래 코드는 비동기 함수인 setTimeout을 이용하여 작성한 예시이다:
console.log('1');
setTimeout(() => {
console.log('2');
}, 100);
console.log('3');
console.log('4');
해당 함수의 출력을 보면 아래와 같음을 알 수 있다.
1
3
4
2
출력을 보면 비동기함수인 setTimeout의 Callback 함수는 동기함수들이 모두 완료된 후에 실행되는 것을 볼 수 있다. 이는 앞서 말한 Javascript의 동작방식을 생각하면 당연한데, Callback queue는 Call stack이 모두 비워진 후에 Callback queue의 Callback 함수를 stack에 추가하기 때문이다. 이는 setTimeout의 delay 시간을 0으로 설정하여도 마찬가지이고, 동기함수들을 비동기함수 밑에 몇개를 추가하던 마찬가지이다. 이처럼 비동기 함수는 Queue를 사용해 non-blocking 방식으로 동작하며, Call stack이 비어 있는 상태에서 Callback 함수가 실행된다. 이는 메인 스레드가 차단되지 않도록 하여, 사용자 경험을 향상시키는 데 중요한 역할을 한다.
Conclusion
동기 함수는 Call stack을 통해 순차적으로 실행되어 각 함수가 완료될 때까지 다음 함수가 대기하는 반면, 비동기 함수는 이벤트 큐를 통해 non-blocking 방식으로 동작하여 작업이 완료된 후 Callback 함수가 실행된다. 이를 통해 자바스크립트는 긴 작업을 수행하면서도 사용자 인터페이스의 응답성을 유지할 수 있다.
Reference
Node.js. (2024). Asynchronous flow control. Retrieved from https://nodejs.org/en/learn/asynchronous-work/asynchronous-flow-control
Mozilla. (2024). Using promises. Retrieved from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
Mozilla. (2024). Introducing asynchronous JavaScript. Retrieved from https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing
Mozilla. (2024). Async function. Retrieved from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
'JS' 카테고리의 다른 글
[JS] Promise와 Async/Await의 이해 (0) | 2024.08.23 |
---|---|
[JS] Javascript에서의 FP (0) | 2024.07.24 |
[JS] Javascript에서의 모듈 시스템 (1) | 2024.07.23 |
[JS] Javascript에서의 OOP (0) | 2024.07.22 |
[JS] Javascript에서의 Memory Management (0) | 2024.07.18 |