거드 2020. 11. 19. 14:20

렉시컬 스코프

클로저를 이해하려면 자바스크립트의 렉시컬 스코프를 이해해야 하는데 자바스크립트는 컴파일 단계에서 소스코드 문자열을 분석하여 의미를 부여하는 렉싱이라는 작업을 하게 되고 이때 스코프가 된다.

function foo() {
  var a = 100;
  function bar() {
    var b = 200;
    console.log(a + b);
  }
  bar();
}
foo();

우리가 위 코드를 실행하면 foo() 메서드 내의 bar() 메서드 안의 console.log() 메서드가 실행되는데 a + b를 구하기 위해 bar()함수의 스코프부터 a변수를 찾게 된다. 찾지 못하였으니 그다음 foo()함수의 스코프 찾게 되고 a변수를 사용하였으니 사용한다. b변수는 bar스코프 내에 있으니 바로 사용하여 300을 console에 출력하게 된다.

이때 주의할 점은 스코프의 검색은 하위에서부터 시작하여 찾는 순간 중단한다는 점과 하위에서 상위로만 가능하고 상위에서 하위스코프의 검색은 불가하다는 점이다.

function foo() {
  var a = 100;
  var b = 500;
  function bar() {
    var b = 200;
    console.log(a + b); //300
  }
}
function foo() {
  var a = 100;
  function bar() {
    var b = 200;
    console.log(a + b); //300
  }
  bar();
  console.log(b); //error, b is not defined
}

클로져란?

클로저는 함수가 속해있는 렉시컬 스코프를 기억하여 함수가 렉시컬 스코프 밖에서 실행될 때에도 스코프에 접근할 수 있게 해주는 것이다.

function foo() {
  var a = 100;
  function bar() {
    console.log(a);
  }
  return bar;
}
var baz = foo();
baz(); // 100

이전의 렉시컬 스코프의 예제와 유사한 코드이다. 내부의 bar()메서드가 실행되기 전에 foo()에서 bar()함수를 리턴하여 baz변수에 저장하였다.
foo()가 끝나면 a변수에 접근할 수 없을 것이라 생각할 수 있으나 자바스크립트에서는 함수를 리턴하고 클로저를 형성하기 때문에 baz()를 호출할 때 렉시컬 스코프에 접근할 수 있고 변수 a에 접근하여 '100'을 출력할 수 있다.

반복문 클로저

function print() {
  var i;
  for (i = 0; i < 10; i++) {
    setTimeout(function () {
      console.log(i);
    }, 100);
  }
}
print();

최근에 만나게 된 예제이자 대표적인 반복문 예제로 0부터 9까지 출력하고 싶지만 10이 열 번 출력된다.
setTimeout에 넘긴 익명함수는 클로저로 매번 상위 스코프인 print에 i변수를 요청할 것이고 첫 0.1초 후에 호출될 텐데 이미 그 짧은시간 동안 i는 10이 되었고 항상 10을 출력하게 되는 것이다.
이를 원래 의도대로 동작하기 위해서는 새로운 스코프를 추가하여 값을 저장하는 방식으로 처리할 수 있다.

function print() {
  var i;
  for (i = 0; i < 10; i++) {
    (function (count) {
      setTimeout(function () {
        console.log(count);
      }, 100);
    })(i);
  }
}
print();

참고문서

You don't know JS 타입과 문법, 스코프와 클로저 - 카일 심슨