9월 모의고사 - 자바스크립트

Develop
2024-09-23

자바스크립트 퀴즈

웹을 떠돌거나, 면접 등에서 자바스크립트 코드를 실행하면 콘솔에 어떻게 출력되는지 물어보는 퀴즈를 만날 수 있습니다.
되게 간단하게 생겼지만, 막상 풀어보면 헷갈리거나 틀리는 경우가 많습니다.
(대부분이 일부러 헷갈리게 만들어진 문제라고 생각합니다😡)
현실에서는 콘솔에 여러 번 찍어보면서 확인할 수 있지만, 면접 혹은 코딩 테스트에서는 그럴 수 없죠.

이번 글에서는 자바스크립트 코드를 실행했을 때 콘솔에 출력되는 결과를 맞추는 퀴즈를 풀어보겠습니다.
(문제는 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. 코드가 차례대로 실행되어 1이 출력됩니다.
  2. console.log('2')는 당장 실행되지 않고 태스크 큐에 추가됩니다.
    현재 태스크 큐: [2출력]
    현재 마이크로태스크 큐: []
  3. console.log('3')은 당장 실행되지 않고 마이크로태스크 큐에 추가됩니다. 현재 태스크 큐: [2출력]
    현재 마이크로태스크 큐: [3출력]
  4. setTimeout(() => console.log('4'), 0)은 당장 실행되지 않고 마이크로 태스크 큐에 추가됩니다.
    현재 태스크 큐: [2출력]
    현재 마이크로태스크 큐: [3출력, setTimeout(4출력)]
  5. console.log('5')는 당장 실행되지 않고 마이크로태스크 큐에 추가됩니다.
    현재 태스크 큐: [2출력]
    현재 마이크로태스크 큐: [3출력, setTimeout(4출력), 5출력]
  6. console.log('6')은 당장 실행되지 않고 태스크 큐에 추가됩니다.
    현재 태스크 큐: [2출력, 6출력]
    현재 마이크로태스크 큐: [3출력, setTimeout(4출력), 5출력]
  7. 콘솔에 7이 출력됩니다.
  8. 콜 스택이 비었으니 마이크로태스크 큐에 있는 코드부터 콜 스택으로 이동하여 실행됩니다.
    현재 태스크 큐: [2출력, 6출력]
    현재 마이크로태스크 큐: [3출력, setTimeout(4출력), 5출력]
  9. 콘솔에 3이 출력됩니다.
  10. setTimeout(4출력)이 실행되어 4출력이 태스크 큐에 추가됩니다.
    현재 태스크 큐: [2출력, 6출력, 4출력]
    현재 마이크로태스크 큐: [5출력]
  11. 콘솔에 5가 출력됩니다.
  12. 이제 태스크 큐에 있는 코드가 콜 스택으로 이동하여 실행됩니다.
    현재 태스크 큐: [2출력, 6출력, 4출력]
  13. 콘솔에 2가 출력됩니다.
  14. 콘솔에 6이 출력됩니다.
  15. 콘솔에 4가 출력됩니다.

해설

이 문제는 자바스크립트의 이벤트 루프와 태스크 큐, 마이크로태스크 큐에 대해 이해하고 있는지 묻는 문제입니다.
일단 비동기 코드는 콜 스택이 비워진 후에 실행된다는 것을 알고 있어야 접근할 수 있습니다.
또한, 조금 더 상세하게 마이크로태스크 큐가 태스크 큐보다 우선순위가 높다는 것을 알고 있어야 하며,
setTimeoutPromise가 각각 어디에 속하는지 알고 있어야 풀 수 있습니다.


문제 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. 차례대로 코드가 실행되어 1이 출력됩니다.
  2. setTimeout의 콜백 함수는 당장 실행되지 않고 태스크 큐에 추가됩니다.
  3. 콘솔에 2가 출력됩니다.
  4. 콘솔에 4가 출력됩니다.
  5. 콜 스택이 비었으니 태스크 큐에 있는 코드가 콜 스택으로 이동하여 실행됩니다.
    이때 에러가 발생하나, 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
  1. 반복문 내부의 setTimeout은 당장 실행되지 않고 태스크 큐에 추가됩니다.
  2. 콘솔에 완료가 출력됩니다.
  3. 콜 스택이 비었으니 태스크 큐에 있는 코드가 콜 스택으로 이동하여 실행됩니다.
  4. 콘솔에 3이 세 번 출력됩니다.
  5. 콘솔에 0, 1, 2가 출력됩니다.

해설

이 문제는 varlet의 차이를 이해하고 있는지 묻는 문제입니다.
(저는 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
  1. 콘솔에 3이 출력됩니다.
  2. asyncFunc 함수가 호출되어 콘솔에 1이 출력됩니다.
  3. await로 인해 현재 실행 중인 asyncFunc의 실행이 일시 중단되고, 자바스크립트 엔진은 다른 작업을 처리할 수 있도록 제어를 반환합니다.
    asyncFuncPromise가 해결될 때까지 기다립니다.(마이크로태스크 큐에 추가) 기다리는 동안 원래의 실행 환경으로 돌아갑니다.
  4. 콘솔에 4가 출력됩니다.
  5. asyncFunc 함수가 다시 호출되어 콘솔에 1이 출력됩니다.
  6. await로 인해 현재 실행 중인 asyncFunc의 실행이 일시 중단되고 마이크로태스크 큐에 Promise가 추가됩니다. 자바스크립트 엔진은 다른 작업을 처리할 수 있도록 제어를 반환합니다.
  7. 이제 콜 스택이 비었으니 마이크로태스크 큐에 있는 코드가 콜 스택으로 이동하여 실행됩니다.
  8. 첫 번째 asyncFuncawait가 해결되어 await 아래에 있던 2가 출력됩니다.
  9. 두 번째 asyncFuncawait가 해결되어 await 아래에 있던 2가 출력됩니다.
  10. 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. 콘솔에 1이 출력됩니다.
  2. setTimeout의 콜백 함수는 당장 실행되지 않고 태스크 큐에 추가됩니다.
    현재 태스크 큐: [setTimeout(2출력 후 Promise.then(3출력))]
    현재 마이크로태스크 큐: []
  3. Promisethen 핸들러는 당장 실행되지 않고 마이크로태스크 큐에 추가됩니다.
    현재 태스크 큐: [setTimeout(2출력 후 Promise.then(3출력))]
    현재 마이크로태스크 큐: [Promise.then(4출력 후 setTimeout(5출력))]
  4. 콘솔에 6이 출력됩니다.
  5. Promisethen 핸들러는 당장 실행되지 않고 마이크로태스크 큐에 추가됩니다.
    현재 태스크 큐: [setTimeout(2출력 후 Promise.then(3출력))]
    현재 마이크로태스크 큐: [Promise.then(4출력 후 setTimeout(5출력)), Promise.then(7출력)]
  6. 콜 스택이 비었으니 마이크로태스크 큐에 있는 코드가 콜 스택으로 이동하여 실행됩니다.
  7. 첫 번째 Promise.then(4출력 후 setTimeout(5출력))이 실행되어 콘솔에 4가 출력됩니다. setTimeout(5출력)은 당장 실행되지 않고 태스크 큐에 추가됩니다.
    현재 태스크 큐: [setTimeout(2출력 후 Promise.then(3출력)), setTimeout(5출력)]
    현재 마이크로태스크 큐: [Promise.then(7출력)]
  8. 두 번째 Promise.then(7출력)이 실행되어 콘솔에 7이 출력됩니다. 현재 태스크 큐: [setTimeout(2출력 후 Promise.then(3출력)), setTimeout(5출력)]
    현재 마이크로태스크 큐: []
  9. 콜 스택이 비었으니 태스크 큐에 있는 코드가 콜 스택으로 이동하여 실행됩니다.
  10. 먼저 setTimeout(2출력 후 Promise.then(3출력))의 콜백 함수가 실행되어 우선 콘솔에 2가 출력됩니다.
  11. 그 다음 Promise.then(3출력)을 마이크로태스크 큐에 추가하고 콜 스택이 비워집니다.
    현재 태스크 큐: [setTimeout(5출력)]
    현재 마이크로태스크 큐: [Promise.then(3출력)]
  12. 콜 스택이 비었으니 마이크로태스크 큐에 있는 Promise.then(3출력)가 콜 스택으로 이동하여 실행됩니다. 콘솔에 3이 출력됩니다.
    현재 태스크 큐: [setTimeout(5출력)]
    현재 마이크로태스크 큐: []
  13. 마지막으로 setTimeout(5출력)의 콜백 함수가 실행되어 콘솔에 5가 출력됩니다.

해설

이 문제는 좀 더럽네요...
차근차근 코드를 읽으면서 얘는 마이크로태스크 큐에 넣고, 얘는 태스크 큐에 넣고, 콜 스택으로 보내면서 콘솔에 출력되는 순서를 확인해 보면 됩니다.
위의 문제들과 비교했을 때, 새로운 개념이 나오는 문제는 아니고 코드를 읽는 능력을 묻는 문제라고 생각합니다.

조금 헷갈리는 부분은 235 순서인데, 콜 스택이 비었을 때 작업을 하나씩 콜 스택으로 가져와 실행한다는 점을 알고 있다면 도움이 될 것 같습니다.
2가 출력되고 Promise.then(3출력)이 마이크로태스크 큐에 추가되면서 콜 스택의 setTimeout 콜백은 작업을 종료합니다. 즉, 콜 스택이 비워지게 됩니다.
이후 콜 스택이 비었으니 마이크로태스크 큐에 있는 Promise.then(3출력)이 콜 스택으로 이동하여 실행되어 콘솔에 3이 출력됩니다.
5를 출력하는 setTimeout 콜백은 태스크 큐에 추가되어 있으므로 우선순위가 낮아 가장 마지막에 출력됩니다.


결론

어떠셨나요? 전부 맞추셨나요?
자바스크립트로 이런 퀴즈나 면접 질문을 만들면 거의 다 실행 컨텍스트, 이벤트 루프 위주인 것 같습니다.
아무래도 핵심 개념이기도 하고, 헷갈리기 딱 좋은 부분이라 그런 것 아닐까요?

해설을 적으면서 큐를 표현하는 게 조금 어려워서 알아보기 쉬운 방식으로 적는 것에 초점을 맞춰 보았습니다.
실제로는 Promise 자체는 즉시 생성 및 실행되지만, Promise의 결과를 처리하는 콜백 함수들이 마이크로태스크 큐에 추가되어 나중에 실행됩니다.
착오 없으시길 바랍니다...🤗