Join MDN and developers like you at Mozilla's View Source conference, 12-14 September in Berlin, Germany. Learn more at https://viewsourceconf.org

클로저는 독립적인 (자유) 변수를 가리키는 함수이다. 또는, 클로저 안에 정의된 함수는 만들어진 환경을 '기억한다'.

어휘의 범위

다음 함수를 생각해보자.

function init() {
  var name = "모질라"; // init에 있는 지역 변수 name
  function displayName() { // 내부 함수, 즉 클로저인 displayName()
    alert(name); // 부모 함수에 정의된 변수를 사용한다
  }
  displayName();
}
init(); 

함수 init()은 지역 변수 name과 함수 displayName()을 정의한다. displayName()은 함수 init() 안에 정의되어 그 함수(init()) 안에서만 사용할 수 있는 내부 함수이다. 함수 displayName() 자신은 지역변수를 가지지 않지만 외부 함수에 정의된 변수에 접근하는 권한이 있어 부모 함수에 있는 변수 name을 사용할 수 있다.

코드를 한번 실행해보라. 잘 동작할 것이다. 이 예제는 함수 스코핑(functional scoping) 을 보여주기 위해 소개했다. 자바스크립트에서 중첩된 함수는 그 함수 외부에서 정의된 변수를 사용할 수 있다. 

클로저

이제 또 다음 예제를 보자.

function makeFunc() {
  var name = "모질라";
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();

이 코드를 실행하면 앞서 예로 들은 init() 함수와 동일한 결과를 보이는걸 알 수 있다(알람창에 "모질라" 문자열이 보일 것이다). 위 예제와 다른 점, 그리고 흥미로운 점은 외부함수의 리턴 값이 내부함수 displayName() 라는 것이다.

이 코드가 여전히 작동하는 것은 직관적이지 않다. 일반적으로 함수안에 정의된 지역변수는 함수가 실행하는 동안에만 존재한다. makeFunc() 함수가 종료될 때 이 함수 내부에 정의된 지역변수는 없어지는게 상식적이다. 이 코드가 문제없이 동작하는 걸 보면 다른 일이 일어나고 있는 것 같다!

이것은 myFunc 함수가 클로저이기 때문이다. 클로저는 두 개의 것(함수, 그 함수가 만들어진 환경)으로 이루어진 특별한 객체의 한 종류이다. 환경이라 함은 클로저가 생성될 때 그 범위 안에 있던 여러 지역 변수들로 이루어진다. 이 경우에 myFuncdisplayName 함수와 "모질라" 문자열을 포함하는 클로저이다.

조금 더 흥미로운 예제를 보자. makeAdder라는 함수이다.

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

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

print(add5(2));  // 7
print(add10(2)); // 12

여기서 인자 x를 받아 새 함수를 반환하는 makeAdder(x) 라고 하는 하나의 인자를 받는 함수를 만들었다. 반환되는 함수는 인자 y를 받아서 x 값과 y 값의 합을 돌려주는 함수이다.

본질적으로 makeAdder는 함수 공장(자신의 인자에 특정 값을 더하는 함수들을 만들어내는 것을 말한다)이다. 위의 예제에서 두개의 함수를 찍어냈다. 첫째는 인자에 5를 더하는 함수이고 둘째는 인자에 10을 더하는 함수이다.

add5와 add10은 둘다 클로져이다. 두 함수는 같은 정의를 가지지만 다른 환경을 저장한다. add5의 환경에서 x는 5이지만 add10 의 환경에서 x는 10이다.

실용적인 클로저

여기까지는 이론이었다. 클로저는 실용적인가? 이제는 실용적인 사용 방법을 알아보자. 어떤 데이터(환경)와 함수를 연관시키는데 클로저를 사용할 수 있다. 이건 객체지향 프로그래밍과 유사하다. 객체지향 프로그래밍에서는 객체가 데이터(그 객체의 속성)와 하나 이상의 메소드를 연관시킨다.

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

웹 프로그래밍에서 이런 일이 많이 일어난다. 많은 자바스크립트 코드가 이벤트를 기반으로 짜여진다. (특정한 동작을 만들고 클릭이나 키보드 누르기에 이 동작을 연결시킨다) 이벤트에 반응하는 코드를 만든다고 할 수 있겠다. 이런 코드들을 콜백(callback)이라고 부른다.

여기에 실용적인 예제가 있다. 페이지의 글자 크기를 조정하는 몇 개의 버튼을 만든다고 생각해보자. body 엘리먼트에 px단위로 font-size를 설정하고 다른 엘리먼트에서는 상대적인 em 단위로 font-size를 설정하면 되겠다.

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

h1 {
  font-size: 1.5em;
}
h2 {
  font-size: 1.2em;
}

이제 body 엘리먼트의 font-size만 바꾸면 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> 

JSFiddle에서보기

클로저를 이용해서 private 함수 흉내내기

몇몇 언어(예를들어 자바)는 같은 클래스 내부의 메소드에서만 호출할 수 있는 private 메소드를 지원한다.

자바스크립트는 이를 지원하지 않지만 클로저를 이용해서 흉내낼 수 있다. private 함수는 코드에 제한적인 접근만을 허용한다는 점 뿐만 아니라 전역 네임스페이스를 깔끔하게 유지할 수 있다는 점에서 중요하다.

아래에 모듈 패턴이라고 알려진 클로저를 통해 몇 개의 public 함수가 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 세 함수가 공유한다.

공유되는 환경은 정의되자마자 실행되는 익명 함수 안에서 만들어진다. 이 환경에는 두 개의 private 아이템이 존재한다. 하나는 privateCounter라는 변수이고 나머지 하나는 changeBy라는 함수이다. 이 두 아이템 모두 익명함수 외부에선 접근할 수 없다. 하지만 익명함수 안에 정의된 세개의 public 함수에서 사용되고 반환된다.

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

익명 함수가 카운터를 정의하고 이것을 counter 변수에 할당한다는 걸 알아차렸을 것이다. 이 함수를 다른 변수에 저장하고 이 변수를 이용해 여러개의 카운터를 만들수도 있다.

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 */

두개의 카운터가 어떻게 독립적으로 존재하는지 주목하라. makeCounter() 함수를 호출하면서 생긴 환경은 호출할 때마다 다르다. 클로져 변수 privateCounter 는 다른 인스턴스를 가진다.

객체지향 프로그래밍을 사용할 때 얻는 이점인 정보 은닉과 캡슐화를 클로저를 사용함으로써 얻을 수 있다.

자주하는 실수: 반복문 안에서 클로저 만들기

자바스크립트 1.7의 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(); 

JSFiddle에서보기

helpText 배열은 세개의 도움말을 정의한다. 각 도움말은 입력 필드의 ID와 연관된다. 이 세개의 정의를 반복하며 입력필드에 onfocus 이벤트가 발생했을 때 입력필드에 해당하는 도움말을 표시한다.

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

이유는 onfocus 이벤트에 지정한 함수가 클로저라는 것이다. 이 클로져는 함수 본체와 setupHelp 함수의 스코프로 이루어져 있다. 세개의 클로져가 만들어졌지만 각 클로저는 하나의 환경을 공유한다. 반복문이 끝나고 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(); 

JSFiddle에서보기

예상한대로 작동한다. 콜백이 하나의 환경을 공유하지 않고 makeHelpCallback 함수가 만든 새로운 환경을 가진다. 이 환경에는 helpText 배열로부터 해당하는 문자열이 help 변수에 담겨있다.

추가로 원문에는 없지만 makeHelpCallback 함수를 이용하지 않고 즉시 실행 함수를 이용하면 아래와 같다.

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(help) {
      return function() {
        showHelp(help);
      }
    })(item.help);
  }
}

setupHelp(); 

성능과 관련해서

클로져가 필요하지 않은 작업인데도 함수안에 함수를 만드는 것은 스크립트 처리 속도와 메모리 사용량 모두에서 현명한 선택이 아니다.

예를들어 새로운 오브젝트나 클래스를 만들 때 오브젝트 생성자에 메쏘드를 정의하는 것 보다 오브젝트의 프로토타입에 정의하는것이 좋다. 오브젝트 생성자에 정의하게 되면 생성자가 불릴때마다 메쏘드가 새로 할당되기 때문이다.

비현실적이지만 설명을 위해 예제를 첨부했다.

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;
};

위의 두 예제에서는 상속된 속성은 모든 오브젝트에서 사용될 수 있고 메쏘드 정의가 오브젝트가 생성될 때마다 일어나지 않는다. 오브젝트 모델에 대한 자세한 설명을 참고하라.

문서 태그 및 공헌자

 이 페이지의 공헌자: joeunha, Kaben, noritersand, kdnmih, teoli, JaehwanLee, jaemin_jo
 최종 변경: joeunha,