JavaScript 함수: 스코핑, 클로저

오늘은 JavaScript 함수의 두 가지 대표적인 특징인 스코핑 규칙과 클로저에 대해 설명합니다.

스코핑(scoping)

JavaScript는 정적 스코핑(static/lexical scoping)하는 언어입니다. 초기 Lisp을 제외하고 동적 스코핑(dynamic scoping)하는 언어는 거의 없기 때문에 “정적” 스코핑이란 말은 크게 의미가 없습니다. 다만, 다른 언어와 달리 블록 스코핑(blocking scoping)이 아닌 함수 스코핑(function scoping)이라는 점이 특이합니다.

예를 들어, 아래 foo() 함수 호출은 x의 값을 리턴하는데, x 값은 1이 아닌 2가 리턴됩니다. 블록 안에서 새로 변수 x를 선언하고 2를 할당했지만, 자바스크립트는 함수 스코핑이기 때문에 함수 foo()x 변수는 단 하나만 존재합니다. 따라서 var x=2는 새로운 변수를 선언이 아니라 단순히 x=2라는 할당문으로 취급됩니다.

function foo() {
    var x = 1;
    {
        var x = 2;
    }
    return x;
}

var x = foo();
console.log(x);
// 2

함수 스코핑이 블록 스코핑에 비해 가지는 장점은 없습니다. 의도한 설계라기 보다는 초기 프로그래밍 언어 설계 오류라고 보는 편이 맞습니다. 실제로 ES6에서는 이 문제를 바로 잡기 위해 let 키워드를 이용한 블록 스코핑을 새로 도입하였습니다. 아래 foo_() 함수는 var 키워드를 let으로 바꾸었을 뿐인데, 1이 리턴되는 것을 확인할 수 있습니다.

function foo_() {
    let x = 1;
    {
        let x = 2;
    }
    return x;
}

var x_ = foo_();
console.log(x_);
// 1

ES6는 이전 버전 JavaScript와 호환성 때문에 varlet 키워드를 모두 허용하지만, 새로 작성되는 코드는 모두 let으로 작성하는 것이 좋습니다.

클로저

JavaScript 함수는 1등 시민(first-class citizen)이기 때문에 string이나 number와 마찬가지로 함수의 인자로 넘기거나, 함수의 리턴값으로 돌려주거나, 변수에 저장하는 등의 일이 가능합니다. 따라서 함수 안에서 numberstring을 생성하고 사용할 수 있는 것처럼 함수 안에서 새로운 함수를 선언하고 사용하는 것도 가능합니다.

function makeAdder() {
    function add(a, b) {
        return a + b;
    }
    return add;
}

var adder = makeAdder();
console.log(adder(1, 2));
// 3

위의 makeAdder() 함수를 호출하면 새로운 함수 add()를 리턴하는데, 이 함수는 인자 a, b를 받아서 합을 리턴하는 함수입니다. 호출 결과를 변수 adder에 저장하면 adder(1,2)처럼 함수 호출도 가능함을 확인할 수 있습니다. 이 경우, 함수 add()makeAdder() 안에 선언되어 있기 때문에 함수 밖에서 접근할 수 없다는 점을 제외하면 최상단에 정의한 함수와 다를 바가 없습니다.

JavaScript는 여기서 한발 더 나아갑니다. 아래 makeAdder() 함수는 인자 a를 받아서 a를 더해주는 함수 addA를 리턴합니다. 여기서 aaddA()에 선언된 변수가 아니라 makeAdder()에 선언된 변수라는 점이 특별합니다. JavaScript 함수는 해당 함수에 참조하고자 하는 변수가 없으면 여기서 포기하지 않고 바깥 함수(enclosing function)들에 있는 변수를 찾는 것입니다.

function makeAdder(a) {
    function addA(b) {
        return a + b;
    }
    return addA;
}

var add1 = makeAdder(1);
console.log(add1(2));
// 3

따라서 makeAdder(1)이 리턴하는 함수는 인자 b를 받아서 1을 더해주는 함수가 됩니다. addA()에서 참조한 변수 amakeAdder() 함수가 리턴된 후에도 addA()에서 사용할 수 있기 때문에 가비지 콜렉션되지 않도록 JavaScript 런타임에 처리를 해줍니다.

우리는 보통 함수를 코드로만 생각합니다. 데이터는 함수 호출 시 인자로 넘기는 것이고 함수 자체는 순수한 코드라고 생각하는 경향이 있습니다. 하지만 위 예에서 볼 수 있듯이 JavaScript 함수는 단순히 코드가 아니라 코드+데이터입니다. addA() 함수는 덧셈을 하는 코드도 가지고 있지만, a의 값인 데이터도 가지고 있기 때문입니다. 이렇게 코드와 데이터를 모두 가지고 있는 함수를 클로저(closure)라고 부릅니다.

클로저가 강력한 이유는 “코드+데이터 = 프로그램”이기 때문입니다. 즉, 클로저만 있으면 모든 프로그램을 표현할 수 있다는 뜻입니다. 오브젝트도 마찬가지입니다. “오브젝트 = 코드+데이터”이고, “코드+데이터 = 클로저”이므로 “오브젝트 = 클로저”라는 공식이 성립합니다.

실제로 JavaScript에서는 클로저를 이용해 오브젝트를 만드는 일이 흔합니다. 클로저를 이용해 counter 오브젝트를 만들면 다음과 같습니다. (이 코드는 MDN 클로저 페이지에서 차용했습니다.) changeBy(), increment(), decrement(), value() 함수 모두 privateCounter 변수에 접근하는 클로저임을 확인할 수 있습니다.

var counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };
})();
Advertisements