티스토리 뷰

JavaScript

클로저 (Closure)

da.som 2018. 12. 9. 02:55

이전에 JavaScript가 일급객체이기 때문에 할 수 있는 것 몇 가지를 나열한 적이 있다. 그 중 다음과 같은 항목이 있다.

'Javascript의 클로저(closure) 를 사용해 커링(currying) 과 메모이제이션(memoization) 이 가능하다'

위 문장 안에는 총 세 가지의 개념이 들어있는데, 우선 JavaScript에서 가장 중요한 개념 중 하나인 클로저(Closure) 에 대해 정리해보려고 한다.

클로저(Closure)

클로저는 매우 다양하게 정의되는데, 제일 간단히 설명하자면 함수내에서 작성된 함수 라고 할 수 있다. 일반적으로 함수 내에서 익명함수 형태로 내부함수를 정의하고 리턴하여 외부함수 바깥에서 사용한다.

하지만 이것만으로 클로저를 설명하기엔 부족한 것 같아 덧붙여보자면,
클로저란 Javascript의 유효범위, 즉 Scope를 이용하여 생명 주기가 끝난 외부함수의 변수를 참조하는 방법 이라 할 수 있다.

function wrapper() {
  var bar = 'closure';

  return function () { // 함수 반환
    return bar;
  }
}

var closureTest = wrapper(); // 반환된 결과를 변수에 저장
console.log(closureTest()); // 내부함수 호출 => 'closure' 출력

위에서 정의한 wrapper() 는 함수를 반환하고, 반환된 내부함수는 외부함수 내부에서 선언된 지역 변수를 참조하고 있다. 여기서 참조된 변수 bar 는 외부함수가 종료된 후 소멸되는 것이 아니라 제대로 된 값을 반환하고 있어 마치 살아있는 것 처럼 보인다.
사실, 실제로는 외부함수의 실행문맥은 스택에서 없어졌지만 함수가 호출될 때 외부함수의 변수 및 함수 정보, 스코프 체인을 저장하고 있는 객체가 살아있기 때문에 값을 참조할 수 있는 것이다. 이렇게 외부함수가 종료된 후에도 사용할 수 있는 외부함수의 인자나 지역 변수를 자유 변수(free variable) 라고 부른다.

그렇다면 클로저가 여러번 호출되면 어떤 상황이 발생하는지 예시를 통해 확인해보자.

var base = 'My name is ';

function sayName(name) {
  var introduce = base + name;

  return function() {
    console.log(introduce);
  };
}

var introduce1 = sayName('철수');
var introduce2 = sayName('유리');
var introduce3 = sayName('짱구');
introduce1(); // My name is '철수'
introduce2(); // My name is '유리'
introduce3(); // My name is '짱구'

여기서 inroduce1() , inroduce2(), inroduce3() 을 호출한 결과를 보면 introduce 변수가 동적으로 바뀌고 있는 것처럼 보이지만,
같은 함수를 통해 받은 리턴 값이어도 각각 다른 스코프 체인 을 가지기 때문에 실제로는 introduce 라는 변수가 여러번 생성된 것이다. 즉, 클로저를 통해 서로 다른 환경 을 가지게 되는 것이다.

다시 클로저의 조건을 정리해보자면 다음과 같다.

  1. 내부함수가 익명함수로 정의되어 외부함수의 return값으로 사용되어야 한다.
  2. 반환되는 내부함수는 외부함수의 실행문맥에서 실행된다.
  3. 내부함수에서 사용되는 변수는 외부함수의 변수 스코프에 있어야 한다.

클로저 활용법

지금까지는 클로저의 개념에 대해 알아봤다면, 그래서 클로저를 어떤 경우에, 어떻게 사용하는 걸까?

1. 현재 상태를 기억하고 변경된 최신 상태를 유지하고 싶을 때
2. 전역변수의 오/남용이 없는 깔끔한 코드 작성

<!DOCTYPE html>
<html>
<body>
  <button type="button" id="btn">Count!</button>
  <p id="result">0</p>

  <script>
      var addBtn = document.getElementById("btn");
    var resultBox = document.getElementById("result");

    var counter = (function() {
      var count = 0;

      // 클로저 반환
      return function() {
          return ++count; // 상태 변경
      };
    })();

    addBtn.onclick = function() {
      resultBox.innerHTML = counter();
    }
  </script>
</body>
</html>

버튼을 클릭하면 즉시실행함수 counter 가 반환한 클로저가 호출되고, 현재 횟수를 나타내는 변수 count 의 값이 변경된다.
변수 count 는 클로저에 의해 참조되고 있기 때문에 자유변수로 사용이 가능하며, 자신의 변경된 최신상태를 계속해서 유지한다. 그렇기 때문에 위와 같이 누적 횟수 구현이 가능하다.

이처럼 클로저는 현재 상태(여기서는 누적 횟수)를 기억하고, 이 상태가 변경되어도 최신 상태를 유지 해야하는 상황에 매우 유용하다.
만약 클로저없이 같은 기능을 구현하려면 전역변수를 사용해야하는데, 전역변수 는 누구나 접근할 수 있기 때문에 사용을 억제 해야한다.

3. 캡슐화와 은닉화
4. 다양한 JavaScript의 디자인 패턴 적용

JavaScript 코드를 객체지향으로 사용하기 위해 Prototype을 이용하는 것을 많이 봤을 것이다.
하지만 Prototype을 통해 객체를 만들 때는 private 변수에 대한 접근 문제가 발생한다.

function sayName(name) {
  this._name = name;
}

sayName.prototype.say = function() {
    console.log('My name is ' + this._name);
}

var introduce1 = new sayName('철수');
var introduce2 = new sayName('유리');
var introduce3 = new sayName('짱구');
introduce1.say(); // My name is '철수'
introduce2.say(); // My name is '유리'
introduce3.say(); // My name is '짱구'

introduce1._name = '바보';
introduce1.say(); // My name is '바보'

위에서 sayName() 으로 생성된 객체들은 모두 _name 이라는 변수를 가지게 된다.
문제는 이 변수를 private 변수로 쓰고 싶지만 외부에서 쉽게 접근이 가능 하다는 것이다.

이 경우에 클로저를 사용하면, 클로저를 통해 만들어진 자유 변수는 객체지향언어의 private 변수와 같은 효과를 내기 때문에 외부에서 변수에 직접 접근하는 것을 제한 할 수 있다.

function sayName(name) {
  var _name = name; // 외부에서 접근할 수 없는 private 변수
  return function() {
    console.log('My name is ' + _name);  
  };
}

var introduce1 = new sayName('철수');
var introduce2 = new sayName('유리');
var introduce3 = new sayName('짱구');
introduce1(); // My name is '철수'
introduce2(); // My name is '유리'
introduce3(); // My name is '짱구'

위와 같이 클로저를 이용해 외부 함수의 변수를 private 으로 지정하여 은닉화 할 수 있고, 모듈 패턴생성자 패턴 과 같은 디자인 패턴 구현 이 가능하다.

클로저 사용 시 주의할 점

앞에서 말했듯이 스코프 체인이 생성될 때마다 변수 값들을 보존하고 기억하기 때문에 함수가 메모리에서 없어질 때까지 따라다니게 된다.
이러한 클로저의 특성 때문에 잘못 사용하면 성능 문제메모리 문제 가 발생할 수 있다.

Reference

댓글