티스토리 뷰

프로그래밍/JavaScript Web App

클로져(Closures) 심화

쇠주는참이슬 2015. 2. 4. 19:18




자바스크립트 개발자라면 클로져에 대해서 꼭 이해해야 합니다.
 


클로져(Closures)


자바스크립트를 배우려는 사람들에게 클로져는 어렵게 느껴지지만 자바스크립트를 깊게 알기 위해서 반드시 넘어야할 산이다.

다음 함수를 생각해보자.

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

init() 함수는 name 이라는 지역변수를 만들고 displayName() 이라는 함수를 정의한다. displayName() 은 내부함수라고 불리는데 이는 함수 init() 안에 정의되었고 init() 함수 안에서만 사용할 수 있기 때문이다. displayName() 함수는 지역변수를 가지지 않지만 외부에서 정의된 name변수를 사용하고 있다.

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

다른 예제를 보자.

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

var myFunc = makeFunc();
myFunc();

이 예제를 실행해 보면 위의 예제 init() 함수와 동일한 결과를 보이는걸 알 수 있다(알람창에 "Mozilla" 문자열이 보일 것이다). 위 예제와 다른 점은 외부함수의 리턴 값이 내부함수 displayName() 라는 것이다. 흥미롭지 않은가?

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

이 퍼즐에 대한 해답은  myFunc 함수가 클로져(closure) 를 갖는다는 것이다. 클로져는 두 개의 것으로 이루어진 특별한 오브젝트이다. 첫 번째는 함수이고 두 번째는 그 함수가 만들어진 환경이다. 그 함수가 만들어진 환경은 함수가 만들어질 때 사용할 수 있었던 변수들로 이루어진다. 이 경우에 myFunc 는displayName 함수와 "Mozilla" 문자열을 포함하는 클로져이다.

조금 더 흥미로운 예제를 보자. 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

이 예제에서 makeAdder(x) 라고 하는 하나의 인자를 받는 함수를 만들었다. 이 함수는 x라는 인자를 받아서 새로운 함수를 반환한다. 반환하는 함수는 y라는 인자를 받아서 x+y를 돌려주는 함수이다.

makeAdder 는 함수 공장(function factory)이다. 특정한 수를 인자에 더해서 돌려주는 함수들을 '찍어낸다'. 위의 예제에서 두개의 함수를 찍어냈다. 첫째는 인자에 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);

size12size14, 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;
    }
  }   
})();

alert(Counter.value()); /* 0 */
Counter.increment();
Counter.increment();
alert(Counter.value()); /* 2 */
Counter.decrement();
alert(Counter.value()); /* 1 */

이전 예제에서는 각 클로져가 자기만의 환경을 가졌지만 이 예제에서는 하나의 환경을 Counter.incrementCounter.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;
};

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

문서 태그 및 공헌자

Contributors to this page: jaemin_joteoliJaehwanLee

출처 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Closures#.EC.84.B1.EB.8A.A5.EA.B3.BC_.EA.B4.80.EB.A0.A8.ED.95.B4.EC.84.9C


댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/12   »
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 31
글 보관함