for문에서 setTimeout과 console 사용하기(var,let)

for문에서 setTimeout을 의도대로 사용하기

다음 두 실행문은 각각 변수로 let과 var를 사용한 점이 다릅니다. 그렇다면 출력되는 결과는 어떨까요?

for (let i=0; i<5; i++){
  setTimeout(()=>{console.log(i)},1000);
} // 0, 1, 2, 3, 4

for(var i=0; i<5; i++){
  setTimeout(()=>{console.log(i)},1000);
} // 5, 5, 5, 5, 5

let을 사용하면 0, 1, 2, 3, 4가 약 1초 뒤 한번에 출력되는 것을 확인할 수 있고 var를 사용하면 5가 다섯 번 출력되는 것을 확인할 수 있습니다.

우선 var는 왜 5를 다섯 번 출력할까요?

이는 이벤트 루프의 처리에 따라 setTimeout 메서드가 호출 스택에서 백그라운드를 거쳐 태스크 큐로 이동했다가 다시 콜 스택으로 돌아오는 동안 for 문이 모두 종료(콜 스택이 비어 있어야 태스크 큐에서 콜 스택으로 이동)되어 출력할 데이터는 i=5인 클로저를 참조하기 때문입니다.

그렇다면 var와 달리 let은 왜 순서대로 숫자가 출력될까요?

이는 스코프와 관련이 있습니다.

var는 함수 스코프를 가지므로 for 루프마다 같은 참조를 바인딩하고 let은 블록 스코프이므로 for 루프마다 새로운 참조를 바인딩하게 됩니다.

그렇다면 var를 사용하더라도 for 루프마다 새로운 참조를 바인딩하면 결과가 달라지지 않을까요?

for(var i=0; i<5; i++){
  setTimeout(console.log.bind(console,i), 1000);
}

for(var i=0; i<5; i++){
  setTimeout(console.log, 1000,i);
}

또는 즉시실행함수(IIFE)사용해도 됩니다.

for (var i=0; i<5; i++) {
  (i => setTimeout(() => console.log(i), 1000))(i);
}

그러나 var는 이제 사용을 권장하지 않으며 let으로 같은 효과를 낼 수 있으므로 구조와 작동 원리 차원에서만 알아두면 좋을 것 같습니다.


그렇다면 잠시 앞으로 돌아가서 왜 setTimeout은 먼 길을 돌아서 실행되는 걸까요?

이벤트 루프가 비동기나 콜백 함수를 모두 백그라운드로 전달하여 작업 효율을 높이려고 하기 때문입니다.

이 흐름을 확인하는 방법은 다음 코드를 통해 확인할 수 있습니다.

for (let i=0; i<3; i++){
  console.log('start');
  setTimeout(()=>{console.log(i)},0);
  console.log('end');
}

이처럼 setTimeout에 0초를 설정해도 작업이 뒤로 밀리는 것을 알 수 있습니다(0이라고 해도 실제로는 4ms 소요).

그럼 i초마다 i를 반환하는 함수를 생성해보겠습니다.

//for 문 사용
for (let i=0; i<5; i++) {
  setTimeout(() => console.log(i), i*1000);
}

// async, await 사용
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
(async function loop() {
    for (let i=0; i<5; i++) {
        await delay(1000);
        console.log(i);
    }
})();

//promise 사용
for(let i=0; i<5; i++){
   new Promise((resolve, reject)=>{
      setTimeout(()=>{     
       	resolve(i);
      }, i*1000);
   }).then((data)=>{
      console.log(data);
   })
}