for(let i=0; i<winBalls.length; i++) {
setTimeout(() => {
showBall(winBalls[i], $result)
}, (i+1) * 1000);
}
다음 코드는 내가 로또 생성기를 만들다 쓴 코드이다.
변수 winBalls는 당첨숫자 6개를 저장한 배열 데이터이고
showBall 함수는 아래와 같이 화면에 당첨숫자가 나오게 해주는 코드이다.
앞에서 작성한 반복문을 잠깐 let에서 var로 바꿔보았다.
for(var i=0; i<winBalls.length; i++) {
setTimeout(() => {
showBall(winBalls[i], $result)
}, (i+1) * 1000);
}
단순히 var로 바꿨을 뿐인데 결과가 완전히 달라진다.
winball[i]와 i를 콘솔로 출력하면 모두 undefined, 6으로 나온다.
for(var i=0; i<winBalls.length; i++) {
setTimeout(() => {
console.log(winBalls[i], i);
showBall(winBalls[i], $result)
}, (i+1) * 1000);
}
// 출력 : undefined 6 이 여섯번출력된다.
왜 모든 i가 6으로 나오는 것일까?
이것이 바로 var와 let의 결정적인 차이이다.
🔭 스코프
변수는 스코프(scope, 범위)라는 것을 가진다. var는 함수 스코프, let은 블록 스코프를 가진다.
예제를 보자.
function b() {
var a = 1;
}
console.log(a);
// 출력 : Uncaught ReferenceError: a is not defined
a를 콘솔로 출력하면 에러가 발생한다. a는 함수 안에 선언된 변수이므로 함수 바깥에서는 접근할 수 없다.
이렇듯 함수를 경계로 접근 여부가 달라지는 것을 함수 스코프라고 한다.
이번에는 if문 안에 var를 넣어 보자.
if(true) {
var a = 1;
}
console.log(a);
// 출력 : 1
var는 함수 스코프라서 if 문 안에 들어 있으면 바깥에서 접근할 수 있다.
그런데 let은 다르다.
if(true) {
let a = 1;
}
console.log(a);
// Uncaught ReferenceError: a is not defined
let의 경우에는 에러가 발생한다. 바로 let이 블록 스코프라서 그렇다.
블록은 if문, for문, while문, 함수에서 볼 수 있는 {} 를 의미한다.
블록 바깥에서는 블록 안에 있는 let에 접근할 수 없다. const도 let과 마찬가지로 블록 스코프를 가진다.
for 문도 보자.
for (var i = 0; i < 5; i++) {}
console.log(i);
// 출력 : 5
var는 블록과 관계가 없어서 문제없이 돌아간다. for 문이 끝났을 때 i가 5가 되어 있다는 점에 주목해라.
let은 에러가 발생한다. for 문 블록 바깥에서 접근했기 때문이다.
위치상으로는 let이 블록 바깥에 있지만, for 문은 블록 안에 있는 것으로 친다.
for (let i = 0 ; i < 5; i++) {}
console.log(i);
// Uncaught ReferenceError: i is not defined
블록 스코프와 함수 스코프에 관해 간략히 알아봤다. 다시 로또생성기 코드를 보자.
for(var i=0; i<winBalls.length; i++) {
setTimeout(() => {
showBall(winBalls[i], $result)
}, (i+1) * 1000);
}
setTimeout의 콜백 함수 안에 든 i와 바깥의 ( i + 1 ) * 1000는 다른 시점에 실행 된다.
( i + 1 ) * 1000는 반복문을 돌 때 실행되고, setTimeout의 콜백 함수는 지정한 시간 뒤에 호출된다.
그런데 반복문은 매우 빠른 속도로 돌아서 콜백 함수가 실행될 때는 이미 i가 6(winBalls.length가 6)이 되어 있다.
실행 순서
i가 0일 때 setTimeout(콜백0, 1000) 실행
i가 1일 때 setTimeout(콜백1, 2000) 실행
i가 2일 때 setTimeout(콜백2, 3000) 실행
i가 3일 때 setTimeout(콜백3, 4000) 실행
i가 4일 때 setTimeout(콜백4, 5000) 실행
i가 5일 때 setTimeout(콜백5, 6000) 실행
i가 6이 됨 조건 false 반복문 종료
1초 후 콜백0 실행(i는 6)
2초 후 콜백1 실행(i는 6)
3초 후 콜백2 실행(i는 6)
4초 후 콜백3 실행(i는 6)
5초 후 콜백4 실행(i는 6)
6초 후 콜백5 실행(i는 6)
따라서 콜백 함수가 실행될 때 i를 콘솔로 출력하면 6이 나오게 된다.
그리고 winBalls는 인덱스가 5까지밖에 없으므로 winBalls[6]은 undefined가 된다.
그렇다면 왜 let을 쓸 때는 이러한 문제가 발생하지 않았을까? let 코드를 보면서 분석해 보자.
for (let i = 0; i < winBalls.length; i++) {
setTimeout(() => {
console.log(winBalls[i], i);
showBall(winBalls[i], $result)
}, (i+1) * 1000))
}
for 문에서 쓰이는 let은 하나의 블록마다 i가 고정된다. 이것도 블록 스코프의 특성이라고 보면 된다.
따라서 setTimeout 콜백 함수 내부의 i도 setTimeout을 호출 할 때의 i와 같은 값이 들어간다.
클로저 문제
그럼 var를 쓴 사람은 비동기 코드(setTimeout의 콜백함수)를 쓰지못할까?? => 클로저 문제 발생
for (var i = 0; i < winBalls.length; i++) {
setTimeout(() => {
console.log(winBalls[i], i);
showBall(winBalls[i], $result)
}, (i+1) * 1000))
}
🤔 클로저란?
- 함수와 함수 바깥의 매개변수와의 관계이다.
for (var i = 0; i < winBalls.length; i++) {
(function(j) {
setTimeout(() => {
console.log(winBalls[j], j);
showBall(winBalls[j], $result);
}, (i+1) * 1000);
})(i);
}
var는 함수 스코프이니까 함수 안에 고정시키면 된다.
함수 매개변수로 j를 넣고 함수의 인수로 i 를 넣으면 i가 j로 전달이 된다.
j는 매개변수인데, 함수 안에 고정된다.(함수 스코프)
그래서 j는 그때그때의 i를 가르키게 된다.
함수를 바로 호출하는 방법
( function ( ) { } )( )
클로저 문제해결!! 👍
🌈 정리
반복문과 var를 쓸 때 항상 스코프,클로저 문제가 생기는 것은 아니다.
setTimeout 같은 비동기 함수와 반복문, var를 만나면 이런 문제가 발생한다.
'Front end > JavaScript' 카테고리의 다른 글
🔖 스프레드 연산자(Spread operator) (0) | 2023.02.27 |
---|---|
🔖 객체 (0) | 2023.02.25 |
🔖 객체,배열 구조분해 (0) | 2023.02.25 |
🔖 고차함수 개념 및 응용 (0) | 2023.02.22 |
🔖 리스너함수, 이벤트 객체 (0) | 2023.02.22 |