티스토리 뷰

스코프

스코프(유효 범위)란 해당 변수가 정의되어 있는 영역, 즉 정의된 변수를 사용할 수 있는 소스코드의 집합.

자바스크립트는 블록 스코프(block scope)가 아닌 함수 스코프를 사용하므로, 함수 내에 정의된 변수는 해당 함수 내에서만 사용할 수 있고 유효하다.

함수 스코프라는 것은 함수 내에서 정의된 변수는 그 함수의 전체에 걸쳐서 유효하다는 뜻이다. (스코프는 유효 범위라는 뜻이니까 함수 스코프 -> 선언된 함수 내라면 어디에서도 유효하다) 이는 변수가 미처 선언되기도 전에 유효하다는 뜻이고, 이런 현상을 호이스팅(hoisting)이라고 일컫는다.

var scope = "global";
function f() {
  console.log(scope); // undefined
  var scope = "local";
  console.log(scope); // local
}

위 예제에서 함수 f 내에 지역 변수 scope이 선언되어 있으므로, 이 변수는 함수 몸체 내부의 맨 꼭대기로 끌어올려진다. 다만 할당까지 끌어올려지지는 않으므로, 윗 줄의 콘솔 로그는 undefined를 출력하고 마지막 줄의 콘솔 로그는 local을 출력한다.

스코프 체인

자바스크립트에서 전역 변수는 전역 객체의 프로퍼티이다. (ECMAScript 명세에 정의되어 있음)

지역 변수는 그런 규정이 없지만, 변수를 각 함수 호출과 연관된 객체(call object)의 프로퍼티로 생각할 수 있다.

지역 변수를 어떤 객체의 프로퍼티로 생각한다면, 자바스크립트의 모든 코드는 스코프 체인을 갖고 있다. 스코프 체인은 해당 코드의 유효 범위(in scope) 안에 있는 변수를 정의하는 객체의 체인, 리스트다.

자바스크립트가 변수 값을 얻으려고 할 때(variable resolution, 변수 해석) 스코프 체인에서 변수를 찾는다. 스코프 체인은 위에서 말했다시피 객체의 리스트이므로, 첫 번째 객체에서 해당 변수를 찾고, 없으면 그 다음 객체에서 해당 변수를 찾고, 여기도 없으면 그 다음 객체에서 찾는 식이다. 리스트의 끝까지 탐색했는데도 그 변수가 없다면 reference error가 발생하는 것이다.

최상위 자바스크립트 코드(어떠한 함수에도 속하지 않는 코드)의 스코프 체인에는 하나의 객체만 있고, 그것이 전역 객체이다. 중첩되지 않은 함수의 스코프 체인은 2개의 객체로 이루어진다. 하나는 함수의 매개변수와 지역 변수를 정의하는 객체고, 다른 하나는 전역 객체다.

함수가 정의될 때, 함수는 스코프 체인을 저장한다.

함수가 호출될 때, 함수는 지역 변수를 보관하는 새로운 객체를 만들고 그 객체를 기존에 만들어둔 스코프 체인에 추가한다.

클로저

클로저란 함수와, 함수의 변수가 해석되는 스코프를 아울러 말한다. 클로저를 이해하기 위해서는 lexical scoping 규칙을 알고 있어야 한다.

lexical scoping(어휘적 유효범위)이란, 함수가 정의된 시점의 스코프 체인을 사용하여 함수가 실행된다는 뜻이다. 여기서 함수가 호출된 시점이 아니라 정의된 시점이라는 것이 중요하다.

function sandwichMaker() {
  var ingredient = "peanut butter";
  function make(filling) {
    return ingredient + " and " + filling;
  }
  return make;
}

var f = sandwichMaker();
f("cream"); // "peanut butter and cream"

sandwichMaker는 이미 호출된 상태이다. 원래 함수와 그 함수 내에 정의된 지역변수는 함수의 실행이 끝나면 사라질 것이라고 생각할 수 있다. 그럼에도 f를 실행했을 때에 sandwichMaker 함수의 지역 변수였던 ingredient 변수를 정상적으로 참조하여 "peanut butter"가 포함된 문자열을 리턴하고 있다.

이는 lexical scoping 규칙에 의거한 클로저의 특성인데, 함수는 정의되었을 때의 스코프 체인을 사용해서 실행된다. 즉 make 함수가 정의된 스코프 체인에서 ingredient 변수는 "peanut butter"로 바인딩 되었고, make가 어디서 호출되든 상관 없이 make가 실행될 때 이 바인딩은 항상 유효하다.

자바스크립트 함수가 호출되면 호출과 관련된 지역 변수를 보관하는 객체가 생성되고, 이 객체는 함수의 스코프 체인에 추가된다. 함수가 리턴되면 객체와 바인딩된 변수는 스코프 체인에서 제거된다. 만약 inner 함수(바깥이 함수로 감싸져 있는 중첩 함수)가 정의되어 있다면 inner 함수에는 스코프 체인에 대한 참조가 있고, 이 스코프 체인은 객체와 바인딩된 변수들을 참조하고 있다. 만약 inner 함수 객체가 outer 함수 내부에서만 사용된다면, inner 함수는 그들이 참조하는 변수들과 함께 가비지 컬렉션된다. 하지만 어떤 함수가 inner 함수를 정의하고 그 함수를 반환하거나 어딘가의 프로퍼티로 저장한다면 함수 외부에 inner 함수에 대한 참조가 생기게 된다. 이 경우 중첩 함수는 가비지 컬렉션 되지 않고 중첩 함수가 참조하는 변수 또한 가비지 컬렉션되지 않는다.

위의 예시로 다시 보자면, sandwichMaker가 실행될 때 지역 변수인 ingredient가 바인딩된 객체가 만들어지고 이 객체가 스코프 체인에 추가된다. sandwichMaker가 반환됐을 때 이 객체 또한 사라져야 하지만, sandwichMaker가 내부에 정의한 inner 함수 make가 반환되어 변수 f에 저장되었으므로, make 함수에 대한 참조가 남아 있으므로 가비지 컬렉션되지 않고, make 함수가 참조하고 있는 ingredient 또한 가비지 컬렉션되지 않는다. (outer 함수가 실행될 때 inner 함수는 outer 함수의 스코프 체인을 보존한다.)

클로저의 특성

같은 함수 내에 정의된 중첩 함수들은 같은 스코프 체인을 공유한다.

function counter() {
  var n = 0;
  return {
    count: function() {
        n++;
    },
    reset: function() {
      n = 0;
    },
  };
}

counter를 호출해서 c라는 변수에 저장한 다음, count와 reset 메서드를 호출해보면 클로저로 내부 상태를 변화시킬 수 있으며(여기서는 프라이빗 변수 n의 상태), count와 reset 메서드가 동일한 내부 변수 n을 공유한다는 것을 알 수 있다.

for (var i = 0; i < 5; i++) {
    setTimeout(() => console.log(i), 3000);
}

위 코드의 결과는 어떨까? 3초 뒤 5를 다섯 번 로그한다. setTimeout 안에 있는 콜백 함수들도 같은 스코프 체인을 공유하여 i의 값을 공유한 것이다.

그러면 좀 더 복잡한 예제를 살펴보자. for 문 안에서 클로저를 만드는 경우이다.

function wrapElements(a) {
  var result = [], i, n;
  for (i = 0, n = a.length; i < n; i++) {
    result[i] = function () { return a[i]; }
  }
  return result;
}
var wrapped = wrapElements([10, 20, 30, 40, 50]);
var f = wrapped[0];
f(); // undefined

for 문 안에서 중첩 함수가 정의되어 있다. 이러면 클로저가 a.length 개수 만큼 생기는 셈이다. 그리고 위에서 말했듯이 같은 함수 내에서 정의된 중첩 함수들은 같은 스코프 체인을 공유한다. 즉 wrapElements 라는 함수 내에서 정의된 중첩 함수들은 모두 동일한 지역 변수 i를 참조한다. 아까 counter 함수에서 count 함수와 reset 함수가 동일한 n을 공유한 것처럼 말이다.

그래서 for문이 돌면서 i가 최종적으로는 5가 된 상태이고 wrapped에 저장된 어떤 함수를 호출하더라도, function () { return a[i]; } 에서 a[i] 는 a[5]가 되므로, undefined를 리턴한다.

그렇지만 이 결과는 우리가 원하는 결과가 아니므로 IIFE(즉시 호출 함수)를 사용해서 코드를 재작성해보자.

function wrapElements(a) {
  var result = [], i, n;
  for (i = 0, n = a.length; i < n; i++) {
    // Wrap here with IIFE
    (function() {
      var j = i; // 여기서 j에 할당해줘야 한다. j에 할당하지 않고 IIFE로 감싸기만 해서는 여전히 i를 wrapElements 함수에서 공유하므로 소용없다.
        result[i] = function () { return a[j]; }
    })();
  }
  return result;
}
var wrapped = wrapElements([10, 20, 30, 40, 50]);
var f = wrapped[0];
f(); // 10

원래 같은 함수 내에서 정의된 클로저들은 같은 스코프 체인을 공유하는데, 여기서는 IIFE로 감싸져 있어서 공유가 불가능하게 된다. 위에서 말했듯이 같은 함수 내부에 정의된 중첩함수들은 같은 스코프 체인을 공유하는데, 여기서는 같은 함수 조건이 충족되지 않는다. 변수 j를 둘러싼 IIFE 함수가 바로 실행되고 사라지기 때문에 루프가 돌 때마다 매번 다른 함수이므로 j는 공유가 불가능하다.

콘솔에서 스코프를 보면 아까보다 스코프가 하나 더 추가되었고, 이 객체에는 j변수의 값을 저장하고 있음을 알 수 있다.

function wrapElements(a) {
  var result = [], i, n;
  for (i = 0, n = a.length; i < n; i++) {
    // Wrap here with IIFE
    function innerWrap(j) {
        result[i] = function () { return a[j]; }
    }
    innerWrap(i);
  }
  return result;
}
var wrapped = wrapElements([10, 20, 30, 40, 50]);
var f = wrapped[0];
f(); // 10

IIFE가 아닌 그냥 함수로 감싸고 호출해도 된다. innerWrap함수 또한 호출되고 사라지므로 매번 다른 스코프를 만들어서 j라는 매개 변수를 공유할 수 없게 할 것이다.

아니면 let 선언자를 사용해도 같은 목적을 달성할 수 있다. 블록 스코프를 사용하여 지역 스코프를 추가하기 때문이다.

function wrapElements(a) {
  var result = [], n;
  for (let i = 0, n = a.length; i < n; i++) {
    result[i] = function () { return a[i]; }
  }
  return result;
}
var wrapped = wrapElements([10, 20, 30, 40, 50]);
var f = wrapped[0];
f(); // 10

Ref

인사이트 - 자바스크립트 완벽 가이드

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함