자바스크립트 퀴즈
웹을 떠돌거나, 면접 등에서 자바스크립트 코드를 실행하면 콘솔에 어떻게 출력되는지 물어보는 퀴즈를 만날 수 있습니다.
되게 간단하게 생겼지만, 막상 풀어보면 헷갈리거나 틀리는 경우가 많습니다.
(대부분이 일부러 헷갈리게 만들어진 문제라고 생각합니다😡)
현실에서는 콘솔에 여러 번 찍어보면서 확인할 수 있지만, 면접 혹은 코딩 테스트에서는 그럴 수 없죠.
이번 글에서는 자바스크립트 코드를 실행했을 때 콘솔에 출력되는 결과를 맞추는 퀴즈를 풀어보겠습니다.
(문제는 claude-3.5-sonnet 모델의 도움을 받아 만들었습니다🤗)
문제 1
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
Promise.resolve().then(() => setTimeout(() => console.log('4'), 0));
Promise.resolve().then(() => console.log('5'));
setTimeout(() => console.log('6'), 0);
console.log('7');정답 및 해설
1
7
3
5
2
6
4- 코드가 차례대로 실행되어
1이 출력됩니다. console.log('2')는 당장 실행되지 않고 태스크 큐에 추가됩니다.
현재 태스크 큐:[2출력]
현재 마이크로태스크 큐:[]console.log('3')은 당장 실행되지 않고 마이크로태스크 큐에 추가됩니다. 현재 태스크 큐:[2출력]
현재 마이크로태스크 큐:[3출력]setTimeout(() => console.log('4'), 0)은 당장 실행되지 않고 마이크로 태스크 큐에 추가됩니다.
현재 태스크 큐:[2출력]
현재 마이크로태스크 큐:[3출력, setTimeout(4출력)]console.log('5')는 당장 실행되지 않고 마이크로태스크 큐에 추가됩니다.
현재 태스크 큐:[2출력]
현재 마이크로태스크 큐:[3출력, setTimeout(4출력), 5출력]console.log('6')은 당장 실행되지 않고 태스크 큐에 추가됩니다.
현재 태스크 큐:[2출력, 6출력]
현재 마이크로태스크 큐:[3출력, setTimeout(4출력), 5출력]- 콘솔에
7이 출력됩니다. - 콜 스택이 비었으니 마이크로태스크 큐에 있는 코드부터 콜 스택으로 이동하여 실행됩니다.
현재 태스크 큐:[2출력, 6출력]
현재 마이크로태스크 큐:[3출력, setTimeout(4출력), 5출력] - 콘솔에
3이 출력됩니다. setTimeout(4출력)이 실행되어4출력이 태스크 큐에 추가됩니다.
현재 태스크 큐:[2출력, 6출력, 4출력]
현재 마이크로태스크 큐:[5출력]- 콘솔에
5가 출력됩니다. - 이제 태스크 큐에 있는 코드가 콜 스택으로 이동하여 실행됩니다.
현재 태스크 큐:[2출력, 6출력, 4출력] - 콘솔에
2가 출력됩니다. - 콘솔에
6이 출력됩니다. - 콘솔에
4가 출력됩니다.
해설
이 문제는 자바스크립트의 이벤트 루프와 태스크 큐, 마이크로태스크 큐에 대해 이해하고 있는지 묻는 문제입니다.
일단 비동기 코드는 콜 스택이 비워진 후에 실행된다는 것을 알고 있어야 접근할 수 있습니다.
또한, 조금 더 상세하게 마이크로태스크 큐가 태스크 큐보다 우선순위가 높다는 것을 알고 있어야 하며,
setTimeout과 Promise가 각각 어디에 속하는지 알고 있어야 풀 수 있습니다.
문제 2
try {
console.log('1');
setTimeout(() => {
throw new Error('에러');
}, 0);
console.log('2');
} catch (error) {
console.log('3', error.message);
}
console.log('4');정답 및 해설
1
2
4
Uncaught Error- 차례대로 코드가 실행되어
1이 출력됩니다. setTimeout의 콜백 함수는 당장 실행되지 않고 태스크 큐에 추가됩니다.- 콘솔에
2가 출력됩니다. - 콘솔에
4가 출력됩니다. - 콜 스택이 비었으니 태스크 큐에 있는 코드가 콜 스택으로 이동하여 실행됩니다.
이때 에러가 발생하나,try-catch블록은 이미 실행 컨텍스트에서 제거되었으므로 에러가 잡히지 않습니다.
해설
문제 1의 심화 버전이라고 할 수 있을 것 같습니다.
비동기 코드가 태스크 큐에 추가되는 것은 문제 1과 동일하지만, 그게 에러라면...?
안타깝게도 try-catch 블록이 이미 실행 컨텍스트에서 제거된 후에 에러가 발생하기 때문에 에러가 잡히지 않습니다.
따라서 console.log('3', error.message)는 실행되지 않습니다.
문제 3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0);
}
console.log('완료');정답 및 해설
완료
3
3
3
0
1
2- 반복문 내부의
setTimeout은 당장 실행되지 않고 태스크 큐에 추가됩니다. - 콘솔에
완료가 출력됩니다. - 콜 스택이 비었으니 태스크 큐에 있는 코드가 콜 스택으로 이동하여 실행됩니다.
- 콘솔에
3이 세 번 출력됩니다. - 콘솔에
0,1,2가 출력됩니다.
해설
이 문제는 var와 let의 차이를 이해하고 있는지 묻는 문제입니다.
(저는 var로 선언된 것을 알아채지 못해 틀렸습니다🥲)
var는 함수 스코프를 가지므로 전역 변수 i가 선언됩니다.
따라서 세 개의 setTimeout 콜백 함수가 모두 동일한 변수 i를 참조하게 됩니다.
i는 반복문이 끝나면 3이 되므로 세 개의 setTimeout 콜백 함수가 모두 3을 출력합니다.
반면, let은 블록 스코프를 가지므로 각각 다른 변수 j를 참조하게 됩니다.
따라서 세 개의 setTimeout 콜백 함수가 각각 0, 1, 2를 출력합니다.
문제 4
async function asyncFunc() {
console.log('1');
await Promise.resolve();
console.log('2');
}
console.log('3');
asyncFunc();
console.log('4');
asyncFunc().then(() => console.log('5'));정답 및 해설
3
1
4
1
2
2
5- 콘솔에
3이 출력됩니다. asyncFunc함수가 호출되어 콘솔에1이 출력됩니다.await로 인해 현재 실행 중인asyncFunc의 실행이 일시 중단되고, 자바스크립트 엔진은 다른 작업을 처리할 수 있도록 제어를 반환합니다.
asyncFunc는Promise가 해결될 때까지 기다립니다.(마이크로태스크 큐에 추가) 기다리는 동안 원래의 실행 환경으로 돌아갑니다.- 콘솔에
4가 출력됩니다. asyncFunc함수가 다시 호출되어 콘솔에1이 출력됩니다.await로 인해 현재 실행 중인asyncFunc의 실행이 일시 중단되고 마이크로태스크 큐에Promise가 추가됩니다. 자바스크립트 엔진은 다른 작업을 처리할 수 있도록 제어를 반환합니다.- 이제 콜 스택이 비었으니 마이크로태스크 큐에 있는 코드가 콜 스택으로 이동하여 실행됩니다.
- 첫 번째
asyncFunc의await가 해결되어await아래에 있던2가 출력됩니다. - 두 번째
asyncFunc의await가 해결되어await아래에 있던2가 출력됩니다. asyncFunc().then(() => console.log('5'));의then핸들러가 실행되어 콘솔에5가 출력됩니다.
해설
이 문제는 async/await의 동작 방식을 이해하고 있는지 묻는 문제입니다.
async/await은 비동기 코드를 동기적으로 작성할 수 있게 해주는 문법입니다.
await 키워드는 현재 실행 중인 함수의 실행을 일시 중단하고, 비동기 코드가 해결될 때까지 기다립니다.
여기서 뽀인트는 await를 만나면 함수의 실행이 일시 중단되고, 다른 작업을 처리할 수 있도록 제어를 반환한다는 점입니다.
문제 5
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => console.log('5'), 0);
});
console.log('6');
Promise.resolve().then(() => console.log('7'));정답 및 해설
1
6
4
7
2
3
5- 콘솔에
1이 출력됩니다. setTimeout의 콜백 함수는 당장 실행되지 않고 태스크 큐에 추가됩니다.
현재 태스크 큐:[setTimeout(2출력 후 Promise.then(3출력))]
현재 마이크로태스크 큐:[]Promise의then핸들러는 당장 실행되지 않고 마이크로태스크 큐에 추가됩니다.
현재 태스크 큐:[setTimeout(2출력 후 Promise.then(3출력))]
현재 마이크로태스크 큐:[Promise.then(4출력 후 setTimeout(5출력))]- 콘솔에
6이 출력됩니다. Promise의then핸들러는 당장 실행되지 않고 마이크로태스크 큐에 추가됩니다.
현재 태스크 큐:[setTimeout(2출력 후 Promise.then(3출력))]
현재 마이크로태스크 큐:[Promise.then(4출력 후 setTimeout(5출력)), Promise.then(7출력)]- 콜 스택이 비었으니 마이크로태스크 큐에 있는 코드가 콜 스택으로 이동하여 실행됩니다.
- 첫 번째
Promise.then(4출력 후 setTimeout(5출력))이 실행되어 콘솔에4가 출력됩니다.setTimeout(5출력)은 당장 실행되지 않고 태스크 큐에 추가됩니다.
현재 태스크 큐:[setTimeout(2출력 후 Promise.then(3출력)), setTimeout(5출력)]
현재 마이크로태스크 큐:[Promise.then(7출력)] - 두 번째
Promise.then(7출력)이 실행되어 콘솔에7이 출력됩니다. 현재 태스크 큐:[setTimeout(2출력 후 Promise.then(3출력)), setTimeout(5출력)]
현재 마이크로태스크 큐:[] - 콜 스택이 비었으니 태스크 큐에 있는 코드가 콜 스택으로 이동하여 실행됩니다.
- 먼저
setTimeout(2출력 후 Promise.then(3출력))의 콜백 함수가 실행되어 우선 콘솔에2가 출력됩니다. - 그 다음
Promise.then(3출력)을 마이크로태스크 큐에 추가하고 콜 스택이 비워집니다.
현재 태스크 큐:[setTimeout(5출력)]
현재 마이크로태스크 큐:[Promise.then(3출력)] - 콜 스택이 비었으니 마이크로태스크 큐에 있는
Promise.then(3출력)가 콜 스택으로 이동하여 실행됩니다. 콘솔에3이 출력됩니다.
현재 태스크 큐:[setTimeout(5출력)]
현재 마이크로태스크 큐:[] - 마지막으로
setTimeout(5출력)의 콜백 함수가 실행되어 콘솔에5가 출력됩니다.
해설
이 문제는 좀 더럽네요...
차근차근 코드를 읽으면서 얘는 마이크로태스크 큐에 넣고, 얘는 태스크 큐에 넣고, 콜 스택으로 보내면서 콘솔에 출력되는 순서를 확인해 보면 됩니다.
위의 문제들과 비교했을 때, 새로운 개념이 나오는 문제는 아니고 코드를 읽는 능력을 묻는 문제라고 생각합니다.
조금 헷갈리는 부분은 2 → 3 → 5 순서인데, 콜 스택이 비었을 때 작업을 하나씩 콜 스택으로 가져와 실행한다는 점을 알고 있다면 도움이 될 것 같습니다.
2가 출력되고 Promise.then(3출력)이 마이크로태스크 큐에 추가되면서 콜 스택의 setTimeout 콜백은 작업을 종료합니다. 즉, 콜 스택이 비워지게 됩니다.
이후 콜 스택이 비었으니 마이크로태스크 큐에 있는 Promise.then(3출력)이 콜 스택으로 이동하여 실행되어 콘솔에 3이 출력됩니다.
5를 출력하는 setTimeout 콜백은 태스크 큐에 추가되어 있으므로 우선순위가 낮아 가장 마지막에 출력됩니다.
결론
어떠셨나요? 전부 맞추셨나요?
자바스크립트로 이런 퀴즈나 면접 질문을 만들면 거의 다 실행 컨텍스트, 이벤트 루프 위주인 것 같습니다.
아무래도 핵심 개념이기도 하고, 헷갈리기 딱 좋은 부분이라 그런 것 아닐까요?
해설을 적으면서 큐를 표현하는 게 조금 어려워서 알아보기 쉬운 방식으로 적는 것에 초점을 맞춰 보았습니다.
실제로는 Promise 자체는 즉시 생성 및 실행되지만, Promise의 결과를 처리하는 콜백 함수들이 마이크로태스크 큐에 추가되어 나중에 실행됩니다.
착오 없으시길 바랍니다...🤗