본문 바로가기
JAVASCRIPT

[JS][스코프 클로저] 반복문과 클로저

by KBS 2022. 2. 18.
728x90

반복문과 클로저

클로저를 설명하는 가장 흔하고 표준적인 사례는 for 반복문이다.

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

 

린터는 반복문 안에 함수를 넣을 경우 종종 경고한다. 이렇게 설정한 이유는 개발자들이 클로저를 자주 사용한다고 생각하지 않기 때문이다. 린터는 개발자가 무엇을 하고 있는지 모른다고 취급하며 그저 경고를 내보낸다.

 

이 코드의 목적은 예상대로 '1', '2', ... , '5'까지 한 번에 하나씩 일 초마다 출력하는 것이다. 그러나 실제로 코드를 돌려보면, 일 초마다 한 번씩 '6'만 5번 출력한다.

..?

먼저, 6이 어떻게 나오는지 알아보자. 반복문이 끝나는 조건은 i<= 5가 아닐 때다. 처음으로 끝나는 조건이 갖춰졌을 때 i의 값은 6이다. 즉, 출력된 값은 반복문이 끝났을 때의 i의 값을 반영한 것이다. 코드를 다시 보면 이 설명이 당연하게 느껴질 것이다.

timeout함수 콜백은 반복문이 끝나고 나서야 작동한다. 사실, 타이머를 차지하고 반복마다 실행되는 것이 setTimeout(..., 0)이었다 해도 해당 함수 콜백은 확실히 반복문이 끝나고 나면 동작해서 결과로 매번 6을 출력한다.

여기서 더 심오한 문제가 제기된다. 애초에 문법적으로 기대한 것과 같이 이 코드를 작동시키려면 무엇이 더 필요할까?

그러기 위해 필요한 것은 반복마다 각각의 i 복제본을 '잡아'두는 것이다. 그러나 반복문 안 총 5개의 함수들은 반복마다 따로 정의됐음에도 모두 같이 글로벌 스코프 클로저를 공유해 해당 스코프 안에는 오직 하나의 i만이 존재한다. 콜백을 죽 이어서 반복문 없이 선언해도 결과는 똑같다.

자, 이제 다시 질문으로 돌아가 보자. 무엇이 더 필요한가? 필요한 것은 더 많은 닫힌 스코프다. 구쳊거으로 말하면 반복마다 하나의 새로운 닫힌 스코프가 필요하다.

IIFE가 함수를 정의하고 바로 실행하면서 스코프를 생성한다고 했다. 시도해보자.

for (var i = 1; i <= 5; i++) {
  (function () {
    setTimeout(function timer() {
      console.log(i);
    }, i * 1000);
  })();
}

 

결과는 작동하지 않는다. 왜 그럴까? 이제 분명 더 많은 렉시컬 스코프를 가지는데 말이다. 각각의 timeout 함수 콜백은 확실히 반복마다 각각의 IIFE가 생성한 자신만의 스코프를 가진다. 그러나 닫힌 스코프만으로는 부족하다. 이스코프가 비어있기 때문이다.

자세히 사펴보자. IIFE는 아무것도 하지 않는 빈 스코프일 뿐이니 무언가 해야 한다. 각 스코프는 자체 변수가 필요하다 즉, 반복마다 i의 값을 저장할 변수가 필욯다ㅏ.

for (var i = 1; i <= 5; i++) {
  (function () {
    var j = i;
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })();
}

 

유레카! 성공했다 !

약간 다른 버전을 선호하는 이들도 있을 것이다.

for (var i = 1; i <= 5; i++) {
  (function (j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

 

물론 이 IIFE는 함수니까 i를 넘길 때 인자의 이름은 j로 할 수 있고 다시 i로 할 수도있다. 어떻게 해도 코드는 잘 작동한다.

IIFE를 사용하여 반복마다 새로운 스코프를 생성하는 방식으로 timeout 함수 콜백은 원하는 값이 제대로 저장된 변수를 가진 새 닫힌 스코프를 반복마다 생성해 사용할 수 있다.

문제는 해결됐다!

다시보는 블록 스코프

이전 문제의 해법을 자세히 살펴보자. 반복마다 IIFE를 사용해 하나의 새로운 스코프를 생성했다. 다시 말하면, 실제 필요했던 것은 반복 별 블록 스코프였다. 이전에 블록을 이용해 해당 스코프에 변수를 선언하는 let선언문을 살펴봤다. 키워드 let은 본질적으로 하나의 블록을 닫을 수 있는 스코프로 바꾼다.

자, 다음의 멋진 코드가 어떻게 잘 작동하는지 보자.

for (var i = 1; i <= 5; i++) {
  let j = i;
  setTimeout(function timer() {
    console.log(j);
  }, j * 1000);
}

그러나 그게 다가 아니다 let선언문이 for 반복문 안에서 사용되면 특별한 방식으로 작동한다. 반복문 시작 부분에서 let으로 선언된 변수는 한 번만 선언되는 것이 아니라 반복할 때마다 선언된다. 따라서 해당 변수는 편리하게도 반복마다 이전 반복이 끝난 이후의 값으로 초기화 된다.

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

멋지지 않는가? 블록 스코프와 클로저가 함께 활약해서 모든 문제를 해결했다.


참고

  • You Don't Know JS - 타입과 문법, 스코프와 클로저( 한빛 미디어 )
728x90

댓글