예전에 이벤트 루프와 관련된 포스팅을 하면서, 자바스크립트가 싱글 스레드의 비동기 방식으로 동작한는 이유가 바로 이벤트 루프 때문이라고 했는데, 이번 [자바스크립트 Deep Dive] 를 공부하면서 비동기를 자바스크립트에서 어떻게 지원하는지 알게 되어 이를 정리하고자 한다.
우선 비동기 방식과 동기 방식에 대해 살펴보자.
동기 방식은 흔히 한 요청이 완료되기 전까지 다음 작업(Task)이 멈춰 있는 것을 의미한다. 예를 들어 어떤 데이터를 참조하여 작업하는 코드가 있다고 가정하자. 만약 해당 데이터가 로드 되기 전까지 해당 코드의 동작을 멈춰야 한다면 이는 동기 방식으로 구현할 수 있다.
이후에 설명하겠지만, 자바스크립트는 기본적으로 동기 방식이다.
이것이 무슨 말인가. 위에서 비동기 방식이라고 했던 것과 다르지 않은가. 아래에서 다시 살펴보자.
따라서 다음과 같이 코드를 작성할 경우, 데이터 로드가 완료된 이후 코드가 실행되게 된다.
...
const data = fetchData();
console.log(data)
...
동기 방식은 이처럼 어떤 코드의 순서를 보장한다는 점이 장점이다. 즉, 반드시 순서를 보장해야 하는 코드의 경우 동기 방식으로 구현하는 것이 좋다.
예를 들어 송금 서비스를 예로 들어보면, 송금을 보내기 전 현재 내 예금 잔액 데이터를 조회한다고 생각해보자. 이 때 만약 비동기 방식으로 나의 예금 잔액 데이터를 불러오기 전에 먼저 송금을 하도록 구현한다면, 만약 나의 예금 잔액이 송금액보다 부족한 경우 아주 큰 문제가 발생할 것이다.
따라서 코드의 순서가 보장되어 안전한 코드를 작성할 수 있다. 그러나 만약 순서가 반드시 보장되지 않아도 되거나, 의도적으로 해당 코드의 실행을 연기하고 싶은 경우 어떨까. 이럴 경우 비동기 방식을 고려해보아야 한다.
비동기 방식은 코드의 실행 종료가 완료되지 않아도 바로 다음 작업으로 실행 순서가 옮겨가는 방식을 의미한다. 즉, 위와 같이 실행 순서가 보장되지 않아도 된다면, 먼저 데이터를 요청한 후에 다음 작업으로 실행 순서를 옮기게 되면 빠르게 코드를 실행할 수 있다.
흔히 동기 방식의 단점을 블로킹(Blocking)이라고 하는데 즉, 실행 흐름이 중단되는 것을 말한다. 반면 비동기 방식은 블로킹이 발생하지 않는다는 장점이 있지만 실행 순서가 보장되지 못한다는 단점이 있다.
위에서 자바스크립트는 기본적으로 동기 방식이라고 했다. 그 이유는 바로 자바스크립트 엔진은 단 하나의 '실행 컨텍스트 스택'을 가지고 있기 때문이다.
실행 컨텍스트 스택이란 실행 컨텍스트를 생성하는 코드(전역 코드, 함수 코드, 모듈 코드, eval 코드)가 실행되게 되면, 실행컨텍스트가 생성되어 실제 코드가 실행되는 영역을 말한다. 이 때 스택 구조 상, 가장 최상단에 있는 실행 컨텍스트가 실행되고 나머지 스택이 차례대로 실행되는 구조이기 때문에 동기 방식으로 동작한다.
그렇다면 자바스크립트는 어떻게 비동기를 지원할 수 있을까?
바로 이벤트 루프와 테스크 큐 덕분이다.
주의할 점은 이벤트 루프와 테스크 큐는 자바스크립트 엔진의 영역이 아닌 브라우저 또는 Node.js 영역이다. 즉, 자바스크립트의 비동기 방식은 자바스크립트 엔진과 브라우저 또는 Node.js의 협력을 통해서 구현되는 것이다.
테스크 큐는 코드가 실행되면서 비동기 함수의 콜백 함수 또는 이벤트 핸들러가 저장되는 저장 공간이다. setTimeout, setInterval 과 같은 함수가 대표적인 비동기 함수이다.
setTimeout(()=>{ console.log('hi') }, 1000);
console.log('hello');
위의 코드를 실행하면 실행 순서는 먼저 'hello'가 출력된 후 'hi'가 출력될 것이다. 그 이유는 먼저 setTimeout 함수가 실행되면, 자바스크립트 엔진은 실행컨텍스트 스택에 setTimeout 함수의 실행컨텍스트를 push하여 실행하고 브라우저에게 호출 스케줄링을 위임하고 해당 함수를 종료시킨다.
브라우저는 setTimeout에 2번째 인수로 전달한 시간 이후에 테스크 큐에 해당 콜백 함수는 push한다.
주의할 점은 1000ms 이후에 테스크 큐에 콜백 함수를 push한다는 의미이지, 1000ms 이후에 콜백 함수의 실행을 보장하는 것은 아니다. 이는 이벤트 루프의 동작 때문이다.
즉, setTimeout의 실행은 자바스크립트 엔진이 실행하지만 호출 스케줄링과 콜백 함수 등록은 브라우저 또는 Node.js에서 진행한다.
이벤트 루프는 테스크 큐에 있는 콜백 함수 또는 이벤트 핸들러가 있는지 확인한 후 해당 콜백 함수와 이벤트 핸들러의 호출 시점을 결정한다.
해당 함수들의 호출시점은 현재 실행 컨텍스트 스택에 실행 중인 실행 컨텍스트가 없는 경우이다. 이 때 이벤트 루프는 테스크 큐에서 FIFO 순서로 함수를 실행 컨텍스트 스택에 push하여 실행시킨다.
따라서 위에서 1000ms 이후에 실행 컨텍스트에 실행 중인 함수가 존재한다면, 테스크 큐에 함수가 push되어 있더라도 실행되지 못한다.
동기와 비동기 방식 중 어느 것이 더 좋거나 나쁜 것은 없다. 다만 상황에 따라서 동기 방식 또는 비동기 방식으로 구현할지를 적절히 구분하는 것이 중요하다. 하지만 자바스크립트로 비동기 방식을 구현할 경우 주의해야 하는 점은 바로 실행 순서를 보장하지 못한다는 점이다.
이는 위에서 살펴본 것과 같이 자바스크립트의 비동기 방식은 테스크 큐와 이벤트 루프의 동작 원리에 따라 구현되어 있기 때문이다. 따라서 항상 비동기 함수의 2번째 인수로 전달한 시간 이후에 실행되리라는 보장이 없으므로 이 점을 주의해서 코드를 작성해야 한다.