Back
Featured image of post 자바스크립트 클로저

자바스크립트 클로저

클로저는 무엇이고 어떠한 경우에 사용할까?

클로저

개념

클로저는 쉽게말해 함수 선언시 생성되는 유효 범위이다.
먼저 자바스크립트에서 클로저를 이해하려면 범위 지정(Lexical Scoping)을 이해하여야 한다.
아래 코드를 보자.

var apple = '🍎';

function displayApple() {
  console.log(apple);
}

displayApple();
//result : 🍎

간단하게 사과를 출력해주는 함수다.
하지만, 정작 함수안에는 apple이라는 변수가 없는데도 불구하고 사과가 출력되었다.
지난번 실행컨텍스트에서 언급했듯이 해당 유효범위에서 식별자를 찾지 못하면 점점 상위 ~ 전역으로 탐색한다. Scope Chain

var displayPrice;
var apple = **'🍎';**
function displayApple(){
	var price = 1000;
	console.log('**🍎');**
	function setPrice(){
		console.log(price + ' won!!');
	}
	displayPrice = setPrice;
}

displayApple();
noticePrice();

그럼 위의 코드를 보면 맨 처음 사과를 보여주는 함수 내부에 사과 가격을 출력해주는 함수도 추가해보았다.
이 함수는 전역 스코프에 정의되어있는 displayPrice에 할당하여 displayApple함수가 시행한 후 호출하였다.
결론적으로 displayApple이 끝난 뒤 호출하는 noticePrice 함수 시행시 유효범위가 끝난 줄 알았던 내부함수가 잘 시행이 되었다!!
어떤 원리로 이미 끝난 함수의 내부 함수를 호출할 수 있는 것일까?

/*
**🍎** 
**🍎 
1000 won!!**
*/

특징

먼저 외부함수(displayApple)내에서 내부함수(setPrice)를 선언하였을 때, 함수만 정의되는 것이 아니라 해당 시점의 유효 범위에 포함하는 모든 변수들도 같이 생성된다.
따라서 외부함수를 먼저 실행하고 끝난 뒤 displayPrice가 참조하고있는 내부함수(setPrice)에 이미 끝난 시점임에도 불구하고 그 시점의 유효 범위에 접근할 수있게된다.
클로저는 언제 어디서든 해당 함수 내부에 있는 스코프에 접근이 가능하지만, 그 만큼 비용이 발생된다는 것을 꼭 명심하자.
(더 이상 해당 함수를 사용하는 곳이 없을 때나 페이지가 언로드될 때 까지 계속 메모리에 남아있는 일종의 족쇄같은 녀석이다.)
또한 클로저는 단순히 해당 시점의 유효 범위 상태를 간직하고 있는 것이 아니라, 외부에 노출하지 않고 해당 유효 범위 상태를 수정할 수도 있다!

클로저는 대체 언제사용할까?

그럼 클로저에 대한 개념은 알겠는데, 이 클로저를 언제 유용하게 사용할 수 있을까?

은닉화

자바스크립트에서는 여타 언어와는 달리 접근 제어자를 명확하게 지원하지 않는다.
클로저를 통해 변수의 유효 범위를 세밀하게 제어할 수 있다.

function Fruits() {
  var fruit = '🍎'; //private

  this.getFruit = function () {
    return fruit;
  };

  this.setFruit = function (item) {
    fruit = item;
  };
}

fruits.getFruit(); //🍎
fruits.fruit; //undefined
fruits.setFruit('🍇'); //사과를 포도로 바꾼다.
fruits.getFruit(); //🍇

위 함수는 과일을 관리하는 생성자 함수이다.
과일을 가져오는 getFruit()메서드와 설정하는 setFruit() 메서드가 존재한다.
함수 수행결과와 같이 getFruitsetFruit를 통해 함수 내부의 변수에 접근하는 것은 가능하지만, 직접적인 변수 접근엔 불가하다.
즉, getFruit와 setFruit가 선언되면서 클로저를 생성함으로써 해당 유효범위에 있는 변수에 접근이 가능해진 것이다.

참조형 반복문

반복문 내에 변수가 특정 변수를 계속 참조하고 있는 상황에서도 클로저를 활용해 유리하게 풀어나갈 수 있다.
만약 setTimeout()함수에서 1초간격으로 특정 코드를 수행하는 로직을 짠다면 (예시로 각 초를 출력하도록..)

function countSec(seconds) {
  for (var i = 1; i <= seconds; i++) {
    setTimeout(function () {
      console.log(i);
    }, i * 1000); //setTimeout에 인자로 들어가는 함수는 변수 i를 참조하지만, 이미 countSet()함수가 종료된 시점이라 최종 값으로 할당되었다..
  }
}

countSec(3); //4..4..4

예상과 달리 ‘4’만 3초동안 반복한다.
이유는 setTimeout() 내부함수에서 생성한 변수가 자신 (생성하는 시점에 저장되어 있던 i) 을 기억하는 것이 아닌 생성된 i를 계속 참조하고 있기 때문에 이런 현상이 발생하는 것이다.
이는 즉시실행함수를 통해 올바르게 동작시킬 수 있는데 반복할 때 마다 유효 범위에 i가 새로 정의되고 원하는 연산 값이 출력되는 것을 볼 수 있다.

function countSec(seconds) {
  for (var i = 1; i <= seconds; i++) {
    (function (curNum) {
      //즉시실행함수를 이용해 외부 변수 i를 복사하여 setTimout() 지역 변수로 끌어와 정상적으로 1,2,3 입력된다!
      setTimeout(function () {
        console.log(curNum);
      }, curNum * 1000);
    })(i);
  }
}

countSec(3); //1..2..3

이러한 현상은 DOM 조작시에서도 발생할 수 있는데, 만약 특정 요소를 반복하면서 접근하는 경우에 발생된다.

<body>
  <button>First</button>
  <button>Second</button>
  <button>Third</button>
</body>
var buttons = document.getElemnetByTagName('button');
for (var i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener('click', function () {
    console.log('clicked!' + i + ' button!!');
  });
}

만약 위와같이 단순하게 반복문으로 요소들의 이벤트를 트리거하는 상황이면, 이벤트 핸들러가 코드를 수행할 때 마지막에 저장된 i를 참조하게 된다.
어떠한 버튼을 눌러도 3번째 버튼이라고 출력 될 것이다.
이 역시 즉시시행함수로 바꿀 수 있다.

var buttons = document.getElemnetByTagName('button');
for (var i = 0; i < buttons.length; i++)
  (function (n) {
    buttons[i].addEventListener('click', function () {
      console.log('clicked!' + n + ' button!!');
    });
  })(i);

참고자료

  • 자바스크립트 닌자 비급 - 클로저와 가까워지기 (John Resig 저)