for 문 안에 let 과 var를 넣으면 뭐가 달라지나?

RookieAND·2023년 2월 21일
2

Solve My Question

목록 보기
18/29
post-thumbnail

📖 Introduction

분명 머리로는 안다고 생각했는데 막상 말로서 뱉어내려니 어려웠다.

테오의 프론트엔드 오픈 채팅방에서 for 문 내에서 var 키워드와 let 키워드로 변수를 선언하고 이를 사용하였을 때 왜 차이점이 발생하는지에 대한 질문이 들어왔다. 물론 머리로는 var 는 함수 스코프이니 그렇지 않나? 라는 생각이 들었지만 이걸 명확히 설명하려니 말이 쉽게 떨어지지 않았다.

따라서 오늘은 왜 두 구문이 출력을 시켰을 때 차이가 발생하는지에 대해서, 그리고 이를 정확히 파악하기 위해서 짤막하게 포스팅을 작성해보려 한다.

✒️ let, var in for loop

✏️ var in for loop

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 만 여섯 번 연속으로 출력된다. 왜 이런 현상이 발생하는지를 정확하게 알기 위해서는 스코프와 실행 컨텍스트의 개념을 알아야 한다.

상단의 코드가 동작하는 과정은 아래와 같다.

  • 현재 for 문 내부에 선언된 변수 ivar 키워드로 선언되었다. 따라서 for 블록 내부가 아닌 전역 스코프에 위치하게 된다.
  • 또한 반복이 종료되더라도 변수 i 는 사라지지 않고 반복으로 인한 결과 값인 6 을 가지게 된다.
  • 이후 모든 코드가 실행되어 콜 스택이 비게 되면 setTimeout 으로 인해 태스크 큐에 등록되어 있던 콜백 함수가 실행된다.
  • 하지만 현재 콜백 함수 컨텍스트 내부에는 변수 i 가 없으므로 스코프 체인을 통해 전역 스코프에 위치한 변수 i 를 참조하여 코드를 실행하게 된다.
  • 배열 data 의 인덱스는 0부터 5까지만 올 수 있다. 하지만 현재 i 의 값은 6이니 존재하지 않는 값을 참조하려 했으므로 undefined 를 출력하는 것이다.

✏️ let in for loop

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 키워드로 변수 (식별자) 를 선언하였으며, 중괄호로 감싸졌기 때문에 매 반복마다 새로운 스코프를 생성 한다. 이것이 중요한 포인트 중 하나이다.

상단의 코드가 동작하는 과정은 아래와 같다.

  • 현재 for 문 내부에 선언된 변수 ilet 키워드로 선언되었다. 따라서 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번 출력되는 결과가 나온다.
  • 왜냐하면 for 문의 경우 매 반복마다 새로운 스코프를 생성하지만 해당 코드의 경우 그렇지 않으므로, setTimeout 내의 콜백 함수들이 동일한 스코프 내의 i 값을 참조하기 때문이다.
  • 따라서 콜 스택이 비워진 후 콜백 함수가 순차적으로 출력되더라도 동일한 스코프 내의 i 값을 참조하고 있으니 최종 연산 결과인 5를 인계 받고, 이로 인해 data[5] 의 값인 5 가 6번 출력된다.

📒 References

profile
항상 왜 이걸 써야하는지가 궁금한 사람

0개의 댓글