클로저

현재 번역은 완벽하지 않습니다. 한국어로 문서 번역에 동참해주세요.

클로저는 독립적인 (자유) 변수 (지역적으로 사용되지만, 둘러싼 범위 안에서 정의된 변수)를 참조하는 함수들이다. 다른 말로 하면, 이 함수들은 그들이 생성된 환경을 '기억'한다.

문법적 스코핑 (유효범위)

다음을 보자:

function init() {
  var name = "Mozilla"; // name은 init에 의해 생성된 지역변수다
  function displayName() { // displayName()은 내부 함수인, 클로저다
    alert(name); // 부모 함수에서 선언된 변수를 사용한다
  }
  displayName();
}
init();

init()은 지역 변수 name과 함수 displayName()을 생성한다. displayName()init() 안에 정의된 내부 함수이며 해당 함수 본문에서만 사용할 수 있다. displayName() 자신은 지역 변수를 가지지 않는다, 그러나 외부 함수의 변수들에 접근할 권한을 가지고 있다. 그래서 부모 함수에서 선언된 변수 name을 사용할 수 있다.

코드를 실행하여 displayName() 함수 안의 alert() 명령어가 성공적으로 부모 함수에서 선언된 name 변수의 값을 출력하는지 관찰해보자. 이것은 함수가 중첩됐을 때 파서(구문 분석 프로그램)가 어떻게 변수 이름을 해석하는지 보여주는 문법적 스코핑(유효범위)의 한 예이다. 문법적 스코핑(유효범위)이 어떤 변수가 정의되는 위치를 그 변수가 사용가능하게 하는 위치를 정의하는 소스 코드 범위 안에서 사용한다는 사실이 '문법적'이라는 단어를 의미한다. 중첩함수는 그들의 외부 유효범위에서 정의된 변수에 접근할 권한을 갖는다.

클로저

이제 다음 예제를 보자:

function makeFunc() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();

이 코드를 실행하면 앞의 init() 예제와 정확히 같은 효과가 있다: 자바스크립트 경고 상자에 "Mozilla" 문자열이 보일 것이다. 다르고 흥미로운 것은 displayName() 내부 함수가 실행되기 전에 외부 함수로부터 반환된 것이다.

한 눈에 봐서는 이 코드가 여전히 작동하는 것이 직관적으로 보이지 않을 수 있다. 몇몇 언어에서는, 함수 안의 지역 변수들은 그 함수가 수행되는 기간 동안에만 존재한다. makeFunc() 실행이 끝나면 name 변수에 더 이상 접근할 수 없게 될 것으로 예상하는 것이 합리적이다. 하지만 코드가 여전히 예상대로 작동하는 것으로 봐서 이것은 분명히 자바스크립트의 경우는 아니다.

그 이유는 자바스크립트의 함수가 클로저를 형성하기 때문이다. 클로저는 함수와 함수가 선언된 문법적 환경의 조합이다. 이 환경은 클로저가 생성된 시점의 범위 내에 있는 모든 지역 변수로 구성된다. 위의 경우, myFunc은 makeFunc이 실행될 때 생성된 displayName 함수의 인스턴스에 대한 참조다. displayName의 인스턴스는 그 변수, name 이 있는 문법적 환경에 대한 참조를 유지한다.그러므로 myFunc가 호출될 때 그 변수, name은 사용할 수 있는 상태로 남게 되고 "Mozilla" 가 alert 에 전달된다.

다음은 조금 더 흥미로운 예제인 makeAdder 함수다:

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

이 예제에서 단일 인자 x를 받아서 새 함수를 반환하는 함수 makeAdder(x)를 정의했다. 반환되는 함수는 단일 인자 y를 받아서 x와 y의 합을 반환한다.

본질적으로 makeAdder는 함수 팩토리다. (특정한 값을 원래 함수의 인자에 덭붙일 수 있는 함수들을 만든다). 위의 예제에서 두 개의 새로운 함수들을 만들기 위해 함수 팩토리를 사용한다. 하나는 원래 인자에 5를 더하고 다른 하나는 원래 인자에 10을 더한다.

add5add10은 둘 다 클로저다. 이들은 같은 함수 본문 정의를 공유하지만 서로 다른 문법적 환경을 저장한다. add5의 환경에서 x는 5이지만 add10의 환경에서 x는 10이다.

실용적인 클로저

클로저는 어떤 데이터(문법적 환경)와 그 데이터에 운용되는 함수를 연관시켜주기 때문에 유용하다. 이건 객체가 어떤 데이터(그 객체의 속성)와 하나 이상의 메소드를 연관시킨다는 점에서 객체지향 프로그래밍과 분명히 같은 맥락에 있다. 

결론적으로 오직 하나의 메소드를 가지고 있는 객체를 일반적으로 사용하는 모든 곳에 클로저를 사용할 수 있다.

이렇게 할 수 있는 상황은 특히 웹에서 일반적이다. 프론트 엔드 자바스크립트에서 우리가 쓰는 많은 코드가 이벤트 기반이다. 우리는 몇 가지 동작을 정의한 다음 사용자에 의한 이벤트 (클릭 혹은 키 누르기 같은)에 연결한다. 우리의 코드는 일반적으로 콜백으로 첨부된다: 이벤트에 응답하여 실행되는 단일 함수다.

여기에 실용적인 예제가 있다. 페이지의 글자 크기를 조정하는 몇 개의 버튼을 추가한다고 가정하자. 이 작업을 수행하는 한 가지 방법은 body 요소의 font-size를 픽셀 단위로 지정하고 상대적인 em 단위를 사용하여 페이지의 다른 요소들의 (예: 헤더) 크기를 설정하는 것이다.

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}

h2 {
  font-size: 1.2em;
}

상호작용적 글자 크기 버튼은 body 요소의 font-size 속성을 변경할 수 있고 em 단위 덕분에 페이지의 다른 요소들의 크기도 변경될 것이다.

여기 자바스크립트 코드가 있다.

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

size12, size14, size16은 body 요소의 글자 크기를 각각 12, 14, 16 픽셀로 바꾸는 함수이다. 이 함수들을 아래처럼 버튼 (이 경우에는 링크)에 연결할 수 있다.

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a> 

클로저를 이용해서 프라이빗 메소드 (private 메소드) 흉내내기

몇몇 언어(예를들어 자바)는 메소드를 프라이빗으로 선언할 수 있는 기능을 제공한다. 이는 같은 클래스 내부의 다른 메소드에서만 그 메소드들을 호출할 수 있다는 의미이다.

자바스크립트는 태생적으로는 이런 방법을 제공하지 않지만 클로저를 이용하여 프라이빗 메소드를 흉내는 것이 가능하다. 프라이빗 메소드는 코드에 제한적인 접근만을 허용한다는 점 뿐만 아니라 전역 네임 스페이스를 관리하는 강력한 방법을 제공하여 불필요한 메소드가 공용 인터페이스를 혼란스럽게 만들지 않도록 한다.

아래는 프라이빗 함수와 변수에 접근하는 퍼블릭 함수를 정의할 수 있는 클로저를 사용하는 방법을 보여주는 코드가 있다. 이렇게 클로저를 사용하는 것을 모듈 패턴이라 한다.

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

console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1

이전 예제에서는 각 클로저들이 고유한 문법적 환경을 가졌지만 이 예제에서는 우리는 counter.increment, counter.decrement, counter.value 세 함수에 의해 공유되는 하나의 문법적 환경을 만든다.

공유되는 문법적 환경은 정의되자마자 실행되는 익명 함수 안에서 만들어진다. 이 문법적 환경은 두 개의 프라이빗 아이템을 포함한다. 하나는 privateCounter라는 변수이고 나머지 하나는 changeBy라는 함수이다. 둘 다 외부에서 직접적으로 익명 함수 접근할 수 없다. 대신에 익명 래퍼에서 반환된 세 개의 퍼블릭 함수를 통해서만 접근되어야만 한다.

위의 세 가지 퍼블릭 함수는 같은 환경을 공유하는 클로저이다. 자바스크립트 문법적 스코핑(유효범위, lexical scoping) 덕분에 세 함수 모두 privateCounter 변수와 changeBy 함수에 접근할 수 있다.

카운터를 생성하는 익명 함수를 정의하고 그 함수를 즉시 호출하고 결과를 counter 변수에 할당하는 것을 알아차렸을 것이다. 이 함수를 별도의 변수 makeCounter 저장하고 이 변수를 이용해 여러 개의 카운터를 만들수 있다.

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();
var Counter2 = makeCounter();
alert(Counter1.value()); /* 0 */
Counter1.increment();
Counter1.increment();
alert(Counter1.value()); /* 2 */
Counter1.decrement();
alert(Counter1.value()); /* 1 */
alert(Counter2.value()); /* 0 */

두 개의 카운터가 어떻게 다른 카운터와 독립성을 유지하는지 주목해보자. 각 클로저는 그들 고유의 클로저를 통한 privateCounter 변수의 다른 버전을 참조한다. 어떤 카운터가 호출될 때던지, 그 문법적 환경은 그 변수의 값의 변화에 따라 변하지만 그 변수 값의 변화는 다른 클로저의 값에는 영향을 주지 않는다.

이런 방식으로 클로저를 사용함으로써 객체지향 프로그래밍의 정보 은닉과 캡슐화 같은 이점들을 얻을 수 있다.

루프에서 클로저 생성하기: 일반적인 실수

ECMAScript 2015의 let 키워드 소개 전에는 클로저와 관련된 일반적인 문제는 루프 안에서 클로저가 생성되었을 때 발생한다.다음 예제를 보자.

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp(); 

helpText 배열은 세 개의 도움말 힌트를 정의한다. 각 도움말은 문서의 입력 필드의 ID와 연관된다. 루프를 돌면서 각 입력 필드 ID에 해당하는 엘리먼트의 onfocus 이벤트에 관련된 도움말을 보여주는 메소드에 연결한다.

이 코드를 사용하면 제대로 동작하지 않는 것을 알게 된다. 어떤 필드에 포커스를 주더라도 나이에 관한 도움말이 표시된다.

onfocus 이벤트에 연결된 함수가 클로저이기 때문이다. 이 클로저는 함수 정의와 setupHelp 함수 범위에서 캡처된 환경으로 구성된다. 루프에서 세 개의 클로저가 만들어졌지만 각 클로저는 값이 변하는 변수가 (item.help) 있는 같은 단일 환경을 공유한다. onfocus 콜백이 실행될 때 콜백의 환경에서 item 변수는 (세개의 클로저가 공유한다) helpText 리스트의 마지막 요소를 가리키고 있을 것이다.

더 많은 클로저를 이용해서 문제를 해결할 수 있다. 특히 위에서 언급한 함수 팩토리를 사용하자.

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp(); 

이것은 예상대로 동작한다. 모두 단일 환경을 공유하는 콜백대신, makeHelpCallback 함수는 각각의 콜백에 새로운 문법적 환경을 생성한다. 여기서 help는 helpText 배열의 해당 문자열을 나타낸다.

익명 클로저를 사용하여 위 코드를 작성하는 또 다른 방법은 다음과 같다.

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    (function() {
       var item = helpText[i];
       document.getElementById(item.id).onfocus = function() {
         showHelp(item.help);
       }
    })(); // Immediate event listener attachment with the current value of item (preserved until iteration).
  }
}

setupHelp();

더 많은 클로저를 사용하는 것이 싫다면 ES2015의 let 키워드를 사용할 수 있다.

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    let item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

위의 경우 var 대신 let을 사용 했음으로 모든 클로저가 블록 범위 변수를 바인딩할 함으로 추가적인 클로저를 사용하지 않아도 완벽하게 동작할 것이다.

성능 관련 고려 사항

특정 작업에 클로저가 필요하지 않는데 다른 함수 내에서 함수를 불필요하게 작성하는 것은 현명하지 않다. 이것은 처리 속도와 메모리 소비 측면에서 스크립트 성능에 부정적인 영향을 미칠 것이다.

예를 들어, 새로운 객체/클래스를 생성 할 때, 메소드는 일반적으로 객체 생성자에 정의되기보다는 객체의 프로토타입에 연결되어야 한다. 그 이유는 생성자가 호출 될 때마다 메서드가 다시 할당되기 때문이다 (즉, 모든 개체가 생성 될 때마다).

비실용적이지만 시범적인 다음 예를 고려하라:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

앞의 코드는 클로저의 이점을 이용하지 않음으로 다음과 같이 다시 쓸 수 있다.

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};

그러나 프로토타입을 다시 정의하는 것은 권장되지 않음으로 기존 프로토타입에 추가하는 다음 예제가 더 좋다.

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

위의 코드는 같은 결과를 가진 더 깨끗한 방법으로 작성할 수도 있다:

function MyObject(name, message) {
    this.name = name.toString();
    this.message = message.toString();
}
(function() {
    this.getName = function() {
        return this.name;
    };
    this.getMessage = function() {
        return this.message;
    };
}).call(MyObject.prototype);

앞의 세 가지 예제에서 상속된 프로토타입은 모든 객체에서 공유될 수 있으며 메소드 정의는 모든 객체 생성시 발생할 필요가 없다. 객체 모델의 세부 사항을 참고하라.

문서 태그 및 공헌자

태그: 
 최종 변경: AlexMin,