분명 머리로는 안다고 생각했는데 막상 말로서 뱉어내려니 어려웠다.
테오의 프론트엔드 오픈 채팅방에서 for 문 내에서 var
키워드와 let
키워드로 변수를 선언하고 이를 사용하였을 때 왜 차이점이 발생하는지에 대한 질문이 들어왔다. 물론 머리로는 var 는 함수 스코프이니 그렇지 않나? 라는 생각이 들었지만 이걸 명확히 설명하려니 말이 쉽게 떨어지지 않았다.
따라서 오늘은 왜 두 구문이 출력을 시켰을 때 차이가 발생하는지에 대해서, 그리고 이를 정확히 파악하기 위해서 짤막하게 포스팅을 작성해보려 한다.
const data = [0, 1, 2, 3, 4, 5];
// 결과 : undefined 가 6번 출력된다.
for (var i = 0; i < data.length; i++) {
setTimeout(() => console.log(data[i]), 1000);
}
// 반복이 종료되었음에도 불구하고 변수 i 는 살아있다. (6)
console.log(i)
상단의 코드는 for 문 내부에 var
키워드로 변수 i
를 생성했고, 각 반복마다 setTimeout
비동기 함수를 호출하여 1초 뒤 순차적으로 data 배열 내부의 요소를 출력하도록 하는 것이 목적이었다.
하지만 예상과는 달리 코드를 실행시키면 undefined 만 여섯 번 연속으로 출력된다. 왜 이런 현상이 발생하는지를 정확하게 알기 위해서는 스코프와 실행 컨텍스트의 개념을 알아야 한다.
상단의 코드가 동작하는 과정은 아래와 같다.
i
는 var
키워드로 선언되었다. 따라서 for 블록 내부가 아닌 전역 스코프에 위치하게 된다. i
는 사라지지 않고 반복으로 인한 결과 값인 6 을 가지게 된다.setTimeout
으로 인해 태스크 큐에 등록되어 있던 콜백 함수가 실행된다. i
를 참조하여 코드를 실행하게 된다.data
의 인덱스는 0부터 5까지만 올 수 있다. 하지만 현재 i
의 값은 6이니 존재하지 않는 값을 참조하려 했으므로 undefined
를 출력하는 것이다.const data = [0, 1, 2, 3, 4, 5];
for (let i = 0; i < data.length; i++) {
setTimeout(() => console.log(data[i]), 1000);
}
// for 문 밖에서는 변수 i 에 대한 접근이 불가능하다.
console.log(i)
반대로 기존의 코드에서 변수 선언을 let
키워드로 하게 되면 비로소 우리가 원하던 로직으로 코드가 정상적으로 작동하게 된다. 그 이유는 let
키워드가 블록 스코프를 가지기 때문이다.
for 문을 통해 반복이 진행되고, 해당 구문은 블록 스코프를 가지는 let
키워드로 변수 (식별자) 를 선언하였으며, 중괄호로 감싸졌기 때문에 매 반복마다 새로운 스코프를 생성 한다. 이것이 중요한 포인트 중 하나이다.
상단의 코드가 동작하는 과정은 아래와 같다.
i
는 let
키워드로 선언되었다. 따라서 for 문이 반복될 때마다 새로운 스코프, 즉 렉시컬 환경을 생성한다.i
는 소멸해야 한다. 하지만 setTimeout
의 콜백 함수가 선언될 당시 변수 i
를 참조하고 있기 때문에 소멸되지 않게 된다. 즉 클로저 의 기능을 하고 있다.setTimeout
로 인해 태스크 큐에 등록되었던 콜백 함수가 실행된다. 이때 각각의 함수는 자신이 선언되었을 당시의 스코프에 있던 변수 i
를 개별적으로 참조한다.i
를 통해 data의 요소인 0 부터 5까지를 순차적으로 출력할 수 있게 된다.각 반복마다 다른 스코프를 가지고 있음에 유의해야 한다.
for (let i = 0; i < 10; i++) {
setTimeout(() => {
i += Math.ceil(Math.random() * 100);
}, 1000);
}
i
를 0부터 9까지 차례대로 증가시키면서 setTimeout
함수를 실행시키는데, 내부의 콜백 함수에서는 인계 받은 i
의 값을 0부터 100 사이의 값으로 재할당하는 과정을 거친다.const data = [0, 1, 2, 3, 4, 5];
{
let i = 0;
setTimeout(() => console.log(data[i]), 1000);
i += 1;
setTimeout(() => console.log(data[i]), 1000);
i += 1;
setTimeout(() => console.log(data[i]), 1000);
i += 1;
setTimeout(() => console.log(data[i]), 1000);
i += 1;
setTimeout(() => console.log(data[i]), 1000);
i += 1;
setTimeout(() => console.log(data[i]), 1000);
}
let
키워드로 변수를 선언했다 하더라도 하나의 스코프에 모든 작업을 처리할 경우 우리가 원하는 결과가 아닌 5 가 6번 출력되는 결과가 나온다.setTimeout
내의 콜백 함수들이 동일한 스코프 내의 i
값을 참조하기 때문이다.i
값을 참조하고 있으니 최종 연산 결과인 5를 인계 받고, 이로 인해 data[5]
의 값인 5 가 6번 출력된다.