본문 바로가기

programming/javascript

자바스크립트 클로저(Closure)

클로저

클로저란 어떤 함수에다가 함수가 선언된 환경(렉시컬 환경, lexical environment)에 대한 참조가 결합 형태를 얘기한다. 다시 말해서, 클로저는 이미 호출이 완료된 외부함수의 스코프에 내부함수가 접근이 가능하도록 한다.

C와 같은 언어를 사용하다가 자바스크립트 클로저를 본 사람들은 이상하게 생각할 수도 있다. 왜 함수가 끝났는데 함수 내부 변수에 접근이 가능한거지? C언어에서는 정적변수를 함수내에서 선언하지 않는 이상, 함수 호출이 끝나면 그 내부 변수에 대해서는 참조를 할 수 없다.

근데 자바스크립트는 가능하다. 알아보자.

 

 렉시컬 스코프 (lexical scope)

렉시컬 스코프란 쉽게 말하면 함수가 어디서 호출되는지에 따라서가 아닌 함수가 선언된 위치를 따라 스코프가 결정되는 것이다. 

 

function init() {
  var name = 'Mozilla'; // 지역변수 name 
  function displayName() { 
    alert(name); // 외부 함수에 선언된 변수를 사용한다.
  }
  displayName();
}
init(); // alert 'Mozilla'

 

위에서 보면 displayName 함수 안에는 name 변수의 선언을 찾을 수 없다. 그래서 함수가 선언된 위치(렉시컬 스코프)의 스코프 체인을 따라 name 변수를 찾으러 올라가보면 var name = 'Mozilla' 라는 구문으로 변수 선언이 되어 있다. 따라서 이를 alert에 넘겨준다.

이런 과정으로 스코프가 정해지는 것을 '렉시컬 스코프' 라고한다. 따라서 displayName의 렉시컬 스코프는 함수가 선언된 init 함수 내의 환경을 말한다.

 

클로저 (Closure)

function makeFunc() { // 외부함수 outer function
  var name = 'Mozilla';
  function displayName() { // 내부함수 inner function
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc(); // function displayName() {}
myFunc(); // alert Mozilla

 

이전 코드와 달라진 점은 makeFunc 함수 안에서 displayName 함수를 리턴한다는 것이다. 다른 언어에서는 사실 호출이 끝난 makeFunc 함수 안의 name 변수에는 접근할 수가 없다. (static 변수로 선언하지 않는 이상)

근데 자바스크립트에서는 된다. 왜? 클로저 때문이다. 클로저는 어떤 함수에다가 그 함수의 선언된 환경(렉시컬 환경, lexical environment)에 대한 참조가 결합된 형태를 얘기한다. 여기서 참조라는 것은 어떤 함수가 선언된 환경에 존재하는 모든 지역변수에 접근이 가능하다는 말이다. 즉 위의 예제에서는 displayName 함수가 선언된 makeFunc 안의 내부 환경에 존재하는 name 변수에 접근할 수 있다는 것이다. (makeFunc 함수가 끝난 이후에도)

 

function makeAdder(x) { // x는 makeAdder의 지역변수
  return function(y) {
    return x + y; // 클로저 때문에 x에 지속적으로 접근가능.
  };
}

var add5 = makeAdder(5); // 5를 더하는 함수
var add10 = makeAdder(10); // 10을 더하는 함수

console.log(add5(2));  // 7 = 2 + 5
console.log(add10(2)); // 12 = 2 + 10

 

makeAdder 함수에서 x 지역 변수를 선언한다. 이 함수에서 반환하는 익명함수는 리턴되면서 x에 접근 가능하기 때문에 클로저가 된다. 

따라서 x 에 5를 넣어주면 그 어떤 수에도 5를 더하는 함수(add5)를 만들 수 있다. add10 함수도 만들었는데, 둘은 똑같이 2를 넣어도 다른 결과를 낸다. 왜 일까? 이유는 클로저가 만들어질 때마다 새로운 렉시컬 환경을 저장하기 때문이다. add5 함수에서는 x 가 5인 환경을 저장했고, add10 함수에서는 x가 10인 환경을 저장한 것이다. 이렇듯이 클로저는 각각 독립적으로 환경을 생성한다.

 

클로저에서 private method 실행하기

JAVA 같은 언어에는 클래스 내에서 변수(필드)를 선언할 때 변수에 대한 접근 권한을 다르게 할 수 있는 접근 제어자(private, public, protected 등)가 있다. 자바스크립트에는? 없다고 볼 수도 있다. 일반적으로 자바스크립트 클래스에서 클래스 변수명 앞에 _ 를 붙이면 

private 이라고 암묵적으로 표현한다고 한다. 최근에는 # 을 붙이면 private 변수가 된다. 근데 아직 모든 브라우저에서 지원하는 부분은 아닌것으로 알고 있다.

private 변수 혹은 메서드는 외부 클래스에서 해당 변수에 접근할 수 없도록 접근을 제한해 둔 것을 말한다. 같은 클래스 내에서만 사용할 수 있도록 하기 위해서이다. 이를 보통 정보의 은닉이라고 표현한다. 오직 특정 메서드를 통해서만 접근할 수 있고, 임의로 수정할 수 없게 하기 위함이다.

아래 예제는 클래스는 아니지만 자바스크립트에서 private 하게 메서드를 쓰는 것을 보여준다. 

var counter = (function() {
  var privateCounter = 0; // lexical environment (변수)
  function changeBy(val) { // lexical environment (메서드)
    privateCounter += val;
  }
 // 리턴하면 리턴한 객체를 통해서만 changeBy 메서드와 privateCounter 변수 에 접근 가능.
  return {
    increment: function() {
      changeBy(1);
    },

    decrement: function() {
      changeBy(-1);
    },

    value: function() {
      return privateCounter;
    }
  };
})();

console.log(counter.value());  // 0.

counter.increment();
counter.increment();
console.log(counter.value());  // 2.

counter.decrement();
console.log(counter.value());  // 1.

IIFE를 활용해서 counter 객체를 생성했는데, 이 객체에는 increment, decrement, value 등의 메서드가 들어있다. 이 메서드들은 IIFE로 실행한 익명함수에 대한 하나의 렉시컬 환경을 저장하고 있기 때문에 privateCounter 변수에 접근이 가능하다. 렉시컬환경 내의 변수와 함수에는 다른 방법을 통해서는 접근할 수 없기 때문에 private 하다고 표현한다.

 

counter가 객체이기 때문에 dot notation을 통해 increment 메서드를 실행하면 privateCounter 변수의 값이 1 더해진다.

그리고나서 value 메서드를 호출하면 값이 1 더해진 것을 확인 할 수 있다.

 

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },

    decrement: function() {
      changeBy(-1);
    },

    value: function() {
      return privateCounter;
    }
  }
};

var counter1 = makeCounter(); // 클로저를 각각 만들어준다. counter1, counter2에 서로 다른 렉시컬 환경을
var counter2 = makeCounter(); // 저장한 클로저가 할당된다.

alert(counter1.value());  // 0.

counter1.increment(); // counter1에서는 두번 증가.
counter1.increment();
alert(counter1.value()); // 2.

counter1.decrement(); // counter1 1번 감소
alert(counter1.value()); // 1.
alert(counter2.value()); // 0. counter2는 한번도 증가 및 감소를 시키지 않았으므로.

위에서 말했듯이, 서로 다른 클로저는 각기 다른 렉시컬 환경을 저장한다. 따라서 독립적으로 변수에 접근이 가능하고, 서로 영향을 미치지 않는다. 

 

클로저 사용시 나타나는 실수

var arr = [];

for (var i = 0; i < 5; i++) {
  arr[i] = function () {
    return i;
  };
}

for (var j = 0; j < arr.length; j++) {
  console.log(arr[j]());
}

위 코드를 보면 배열 arr에는 i 값을 리턴하는 함수가 담기고 아래 반복문에서는 0부터 4까지 차례대로 출력할 것 같지만 실행해보면 결과는 5가 5번 나온다. 이유는 i 변수가 var로 선언되어 전역 변수가 되었기 때문이고, 배열에 저장된 함수는 나중에 실행될 때 그 시점의 i값을 참조하기 때문이다. 첫번째 반복문이 끝나면 i는 전역변수로 5의 값을 가지고 각 배열에 저장된 함수는 저 i 값을 참조한다. 

이것을 클로저를 활용해서 해결할 수 있다.

 

var arr = [];
var makeFunc = function(i) {
    return function () {
        return i;
    }
}

for (var i = 0; i < 5; i++) {
  arr[i] = makeFunc(i);
}

for (var j = 0; j < arr.length; j++) {
  console.log(arr[j]());
}

위와 같이 makeFunc 함수를 활용하여 해당 함수를 클로저로 만들어서 렉시컬 환경의 i 저장하게 하면 클로저를 이용해서 문제를 해결 할 수 있다.

그리고 var 키워드를 써서 자바스크립트의 함수 스코프라는 특성때문에 i가 전역변수가 된 탓에 원하는 결과를 얻지 못한 부분도 있다. 따라서 var 대신 let 키워드를 쓰면  간단하게 해결가능하다. 

 

var arr = [];

for (let i = 0; i < 5; i++) {
  arr[i] = function () {
    return i;
  };
}

for (var j = 0; j < arr.length; j++) {
  console.log(arr[j]());
}

 

클로저를 직접 눈으로 확인하고 싶어서 한번 크롬 개발자도구로 클로저로 렉시컬 환경이 저장이 되는지 열어보았다.

 

크롬 개발자도구

 

물론 개발자도구에서 제공하는 기능이겠지만, i 값을 기억하고 있다는 사실을 직접 눈으로 확인해보았다. 

클로저는 이와 같이 외부함수가 종료되어도 렉시컬 환경의 어떤 변수를 내부함수가 참조하는 한 스코프 체인을 따라서 접근이 가능한 것을 말한다.

 

클로저가 개발과정에서 쓰인 예제에는 커링 패턴과, HOF(고차함수) 개념이 있다. 이 부분에 대해서는 다음 글에서 다뤄보려한다.

 

틀린부분이 있다면 지적 부탁드립니다.

 

참조

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

 

Closures - JavaScript | MDN

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).

developer.mozilla.org

https://poiemaweb.com/js-closure

 

Closure | PoiemaWeb

클로저(closure)는 자바스크립트에서 중요한 개념 중 하나로 자바스크립트에 관심을 가지고 있다면 한번쯤은 들어보았을 내용이다. execution context에 대한 사전 지식이 있으면 이해하기 어렵지 않

poiemaweb.com