먼저 console.log('code starts')를 만나면 콜 스택에 스택 프레임을 만들고 실행됩니다. 콘솔 창에 code starts가 출력되겠군요. 다음으로 setTimeout이 콜 스택에 스택 프레임을 만들고 실행됩니다. 이때 setTimeout을 만난 내부 스레드는 타이머를 작동시키며 setTimeout에 전달된 콜백 함수를 스레드에 저장시키며 지연 시간 동안 대기합니다. setTimeout은 할 일을 다했으니 종료되어 콜 스택에서 사라집니다. console.log('code ends')가 콜 스택에 스택 프레임을 만들고 실행됩니다. 콘솔 창에 code ends가 출력된 후 콜 스택에서 사라지겠죠. 이제 콜 스택이 비었습니다.
큐가 등장하는 지금부터가 아주 중요합니다. 곧 타이머가 종료되고 setTimeout에 전달되었던 콜백 함수가 큐에 추가됩니다. 타이머가 종료된다고 바로 스택에 가서 실행될 수는 없습니다. 반드시 이 큐를 거치게 됩니다. 큐에서 대기하는 함수는 오로지 콜 스택이 비어 있을 때만 콜 스택에 스택 프레임을 생성하고 실행할 수 있습니다. 조금 전에 콜 스택이 비었다고 했죠. 이제 setTimeout에 전달되었던 콜백 함수가 큐를 빠져나와 콜 스택에 올라가고 실행됩니다. callback executed를 출력하며 실행이 마무리됩니다. 자바스크립트가 싱글 스레드이지만 비동기로 작동할 수 있는 이유는 바로 내부 스레드와 콜백 함수를 저장해 두는 큐 덕분입니다. 콜 스택이 빌 때 큐에 실행해야 할 콜백 함수가 있다면 꺼내서 콜 스택에 올리는 것입니다.
여기서 한 가지 의문이 드네요. 큐에 타임아웃이 다 된 콜백 함수가 있지만 콜 스택이 비지 않는다면 어떻게 될까요? 실험으로 직접 확인해 봅시다. 다음 코드를 보죠.