스코프, 비동기, var, for문(반복문)과 비동기를 함께 사용하면 종종 발생하게 된다.
function test() {
for(var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000)
}
}
test();
// 내가 기대한 출력값 0 1 2 3 4
// 실제 출력값 5 5 5 5 5
var
는 함수 스코프 변수(function-scope)를 생성한다. 따라서 for문 내에서var
로 선언된 변수i
는 사실상 함수 전체에서 접근이 가능하다. 이것은setTimeout
콜백 함수 내에서i
에 접근할 때, 이미 반복이 끝난 후의i
값을 참조하게 된다.
**var
는 for문 안에 있는 것이 아닌 함수 안에 있는 것이다.
setTimeout
은 비동기로 작동하는 함수이므로 즉시 실행되지 않고 백그라운드에서 실행되고 지정된 시간이 경과한 후 콜백 함수를 호출한다.setTimeout
콜백 함수는 반복문이 이미 완료된 후에 호출된다. 따라서 모든 콜백 함수는 같은i
변수를 참조하게 되고, 그 시점에i
는 5가 된다.
클로저란 함수 외부에 있는 변수와의 관계이다. 내부함수가 외부함수의 접근할 수 있는 변수나 매개변수에 접근하는 것을 말한다. 위 코드에서
setTimeout
콜백 함수는 사실상test()
함수의 내부 함수다.
let
으로 변경해준다.➡️ let
은 ES6에서 추가된 블록 단위 스코프(block scope)를 가지는 키워드로 각 반복마다 고유한 스코프를 가진 변수(i)를 생성한다.
function solved1() {
for(let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000)
}
}
➡️ 즉시 실행 함수(IIFE, Immediately Invoked Function Expression)를 사용하면, 각 반복마다 새로운 스코프가 생성되어 i의 현재 값을 고정하는 효과를 얻을 수 있다.
➡️ (function(j) { // 코드 })()
함수는 정의되자마자 바로 호출된다. 이 경우, i의 현재 값이 즉시 실행 함수에 인자로 전달되어 그 값이 각각의 함수 스코프 내에 고정된다.
➡️ 아래의 (function(j) { })(i)
부분은 i값을 받아서 즉시 실행하는 함수다. 이와 같이 사용하면 각 반복마다 j라는 이름의 새 변수가 생성되고 해당 변수는 자신만의 스코프 내에서 setTimeout
에 의해 만들어진다. 즉, setTimeout
이 실제로 호출될 때 각각의 콜백이 참조하는 j 값은 그 시점에서의 i 값으로 고정된다.
function solved2() {
for(var i = 0; i < 5; i++) {
(function(j) {
setTimeout(() => {
console.log(j);
}, i * 1000)
})(i);
}
}
// 즉시 실행 함수가 외부 변수인 var i와 클로저 관계를 형성하고 있는 것
(function(j) {
setTimeout(() => {
console.log(j);
}, i * 1000)
})(i);
// setTimeout 내부의 함수가 외부 변수인 j와 클로저 관계를 형성하고 있는 것
() => {
console.log(j);
}