근원적인 원인은 var로 선언된 변수는 함수 스코프를 가지기 때문입니다. 이게 뭔 소리일까요? 일반적인 다른 언어의 경우 변수는 블럭 스코프를 가집니다.
먼저 다른 언어에서 변수의 스코프가 어떻게 작동하는지 보기 위해 아래의 C 언어 예제를 보도록 합시다.
#include <stdio.h>
int main(){
int v = 10;
for(int i = 0; i < 4; i++){
int v = i;
printf("%d\n", v);
}
printf("%d\n", v);
return 0;
}
|
cs |
이 예제는 for문 밖에서 v라는 이름으로 선언된 변수와 for문 안에서 v라는 이름으로 선언된 변수의 동작이 어떻게 되는지 보기 위해 만들었습니다. 이름이 겹치고 있는 이 상황. 어떻게 처리될까요?
실행해 보면 이 예제의 출력 결과는
0
1
2
3
10
입니다.
for문에 진입하기 전에 v의 값은 10이었으니 for문 안에서 v에 이런 저런 조작을 해도 for문 밖의 v에는 영향을 미치지 않습니다.
이건 C언어에서는 변수가 "블럭 스코프"를 가지기 때문입니다. 변수는 자기가 선언된 블럭 안에서만 영향을 미칩니다. 이름이 중복되는 경우 더 깊은 블럭의 변수가 우선됩니다.
그림으로 그려보면 이렇게 됩니다. 초록색 스코프에서 v에 대해 일어나는 일은 파란색 스코프의 v에는 그 어떠한 영향도 미칠 수 없습니다.
for문 안의 v와 for문 밖의 v는 이름만 같지 사실 서로 다른 변수입니다.
자바스크립트의 경우에는 다릅니다. var로 선언된 변수의 경우 "함수 스코프"라는 조금은 특이한 스코프를 가집니다. 아까의 에제를 자바스크립트 버전으로 바꾸어 보겠습니다.
function main(){
var v = 10;
for(var i = 0; i < 4; i++){
var v = i;
console.log(v);
}
console.log(v)
}
main();
|
cs |
아까의 예제가 자바스크립트로 바뀌었을 뿐입니다.
이걸 실행한 결과는
0
1
2
3
3
입니다.
결과가 다릅니다!
마지막 줄에서 v를 출력했을 때 v의 원래 값인 10이 아닌 for문 안에서 마지막으로 업데이트된 3이 출력되었습니다. 즉, for문 안의 변수 v는 사실 for문 밖의 변수 v와 동일한 변수임을 보여주고 있습니다. 이건 var로 선언되었을 경우 함수 스코프를 가지기 때문입니다.
함수 스코프란 이름에서도 알 수 있듯이 자바스크립트에서는 변수가 어디서 선언되던 함수 전체의 스코프를 가집니다.
그림으로 그려보면 다음과 같습니다. for문 안에서 선언된 변수는 사실 for문 밖의 변수와 동일한 변수입니다.
var의 이러한 특징 덕분에 다른 언어에서는 안 되는 일도 가능합니다.
function main(){
if(true){
var v = 100;
}
console.log(v)
}
main();
|
cs |
위의 코드의 경우 v는 if문 안에서 선언되었지만 실행해 보면 if문 밖에서 100이 출력되는 것을 확인하실 수 있습니다. 다른 언어였으면 선언되지 않은 변수라는 에러가 떴겠죠.
당근마켓님께서 올려 주신 예제로도 확인해 보겠습니다.
function test(){
var arr = new Array();
arr.push(0);
arr.push(1);
arr.push(2);
arr.push(3);
for (var i = 0; i < arr.length; ++i){
var item = arr[i];
setTimeout(function (){
console.log("var : " + item);
}, (i * 10));
}
}
test();
|
cs |
(var에 집중하기 위해 let은 지웠습니다.)
실행해 보면 var: 3이라는 결과만 4번 나옵니다. 원래 의도했던 것은 일정 시간 간격을 두고 값이 1씩 증가하면서 출력되는 것이었을 것입니다. 하지만 3만 출력됩니다. 이 이유를 이제 위에서 설명한 함수 스코프의 관점에서 설명할 수 있으실 것입니다.
for문의 첫 반복에서 item은 0으로 업데이트 됩니다. 그리고 0초 뒤에 콜백 함수를 예약합니다.
두 번째 반복에서 item은 1로 업데이트 됩니다. 그리고 10ms 뒤에 콜백 함수를 예약합니다.
세 번쨰 반복에서 item은 2로 업데이트 됩니다. 그리고 20ms 뒤에 콜백 함수를 예약합니다.
네 번째 반복에서 item은 3으로 업데이트 됩니다. 그리고 30ms 뒤에 콜백 함수를 예약합니다.
test 함수의 실행이 끝나고 메인 쓰레드가 끝났을 때 이벤트 루프에 의해 예약되었던 콜백함수들이 차례대로 실행됩니다.
0초 뒤에 실행되기로 예약되었던 콜백함수가 실행되었습니다. 이 함수를 item의 값을 출력하려고 합니다. 그런데 이 시점에서 item의 값은 현재 3입니다. 따라서 3이 출력됩니다.
10ms 뒤에 실행되기로 예약되었던 콜백함수가 실행되었습니다. 이 함수를 item의 값을 출력하려고 합니다. 그런데 이 시점에서 item의 값은 현재 3입니다. 따라서 3이 출력됩니다.
20ms 뒤에 실행되기로 예약되었던 콜백함수가 실행되었습니다. 이 함수를 item의 값을 출력하려고 합니다. 그런데 이 시점에서 item의 값은 현재 3입니다. 따라서 3이 출력됩니다.
30ms 뒤에 실행되기로 예약되었던 콜백함수가 실행되었습니다. 이 함수를 item의 값을 출력하려고 합니다. 그런데 이 시점에서 item의 값은 현재 3입니다. 따라서 3이 출력됩니다.
따라서 var: 3이라는 메세지만 3번 출력되는 것입니다.
이 문제는 let을 쓰면 해결되지만 만약 내가 var를 너무나도 사랑하는 프로그래머라 var만을 쓰고 싶을 경우 어떻게 해결할까요?
강제로 새로운 함수 스코프를 설정해 주면 해결이 됩니다.
function test(){
var arr = new Array();
arr.push(0);
arr.push(1);
arr.push(2);
arr.push(3);
for (var i = 0; i < arr.length; ++i){
var item = arr[i];
(function(item){
setTimeout(function (){
console.log("var : " + item);
}, (i * 10));
})(item);
}
}
test();
|
cs |
코드를 약간 바꾸었습니다. 해 준 일은 setTimeout을 호출하던 부분을 즉시 실행 함수로 정의한 뒤 실행한 것입니다. 즉시 실행 함수를 실행할 때 함수 패러미터로 item의 값을 주고 있습니다.
이걸 실행해 보면
var: 0
var: 1
var: 2
var: 3
이라는 결과가 출력됩니다.
이번에 또 왜 되는 걸까요?
즉시 실행 함수를 정의해 줌으로써 item의 스코프를 새로 잡아주었기 때문입니다.
여러 번 이야기하지만 var은 "함수 스코프"를 가집니다. 함수 안에 함수가 또 있을 경우 (자바스크립트는 이게 되죠) 내부 함수 안에서 var로 선언된 변수는 내부 함수까지만 스코프를 가집니다.
이제 settimeout의 콜백 함수가 참조하는 item이라는 변수는 for문에서 선언된 (10번째 줄) item이 아니라 12번째 줄에서 선언된 item을 가리킵니다.
for문이 한 번 반복될 때마다 즉시 실행 함수가 한 번씩 실행되고 그 때마다 서로 다른 item이라는 변수가 생깁니다. 따라서 콜백 함수들이 가리키는 item은 서로 다른 변수이기 때문에 값이 정상적으로 출력되는 것입니다.
이것으로 var의 동작에 대한 설명을 마치겠습니다.
코드 구성을 바꾸는 것으로 어느 정도 let과 var은 치환이 되겠지만 그냥 let을 쓰시는 것이 정신 건강에 이로울 것입니다.