자바스크립트 클로저 개념 이해

YooSeok2·2022년 8월 11일
0
post-thumbnail

소개

오늘은 자바스크립트의 중요한 개념인 클로저를 알아보는 시간을 갖겠습니다.

클로저란?

클로저는 자바스크립트의 고유의 개념은 아니나 자바스크립에 관심을 가지고 있다면 한번쯤은
들어보셨을 겁니다. 프로그래밍 하면서 여러 용도에서 자주 쓰이는 중요한 개념인데요.

MDN에서는 아래와 같이 정의하고 있습니다.
클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다.

문장이 난해하여 이해하기 어려우실거란 생각이 듭니다. 아래의 코드를 통해 이해해봅시다.


  function outerFunc(){
    var num = 1;
    return function innerFunc() {
      console.log(num);
    }();
  };

  var inner = outerFunc(); 
  inner(); //결과 값: 1

outerFunc함수가 내부함수인 innerFunc함수를 리턴하는 간단한 코드입니다. 여기서 주목할 점은 outerFunc는 innerFunc를 반환하고 생을 마감했으나 innerFunc에서 외부함수인 outerFunc함수에 변수num의 복사본이 아닌 *실제 변수에 접근할 수 있다는 것입니다.

이처럼 외부함수가 종료된 시점에서도 내부함수가 유지가 된다면 외부 함수 밖에서 호출되더라도 외부 함수에 지역 변수에 접근할 수 있는데 이를 클로저라고 합니다.

그럼 다시 MDN의 정의로 돌아와서 문장에서 말하는 "함수"란 내부함수를 의미하고 "렉시컬 환경"란 내부 함수가 선언됐을 떄의 스코프를 의미합니다. 다시 말해 클로저는 자신이 선언됐을 때의 스코프를 기억하고 이로인해 밖에서 호출되어도 그 스코프에 접근할 수 있는 함수를 말합니다.

스코프가 뭐야?

스코프는 함수가 선언될 때 결정되는 환경을 의미합니다. 위의 코드에서 예를 들면 innerFunc함수는 outerFunc의 내부에서 선언되었기 때문에 상위 스코프 outerFunc이고 outerFunc은 전역에
선언되었음으로 상위 스코프는 전역 스코프가 됩니다.

그렇다면 외부 함수가 종료되었는데도 지역 변수에 접근할 수 있는 이유는 뭘까?

이는 가비지 콜렉션(GC)에 특징 때문입니다. GC는 어떤 값을 참조하는 변수가 하나라도 있으면 그 값을 수집 대상으로 삼지 않습니다. 위의 코드에서 외부함수는 호출되면서 라이프 사이클이
종료되었지만 내부함수에서 외부함수의 지역 변수의 실제 값을 사용하기 때문에 정리되지
않고 여전히 남아있을 수 있었고 활용이 가능하게 된겁니다.

클로저의 활용

클로저는 자신이 생성될 때의 환경을 기억하면서 메모리를 차지함으로 손해를 볼 수 있으나,
이를 상회하는 자바스크립트의 강력한 기능으로서 유용하게 활용되기 때문에 적극적으로 사용해야 합니다.

아래는 클로저를 유용하게 활용하는 대표적인 사례입니다.


 	var incleaseBtn = document.getElementById('inclease');
    var count = document.getElementById('count');
    	
    var counter = 0; // 카운트 상태를 유지하기 위한 전역 변수

    function increase() {
      return ++counter;
    }

    incleaseBtn.onclick = function () {
      count.innerHTML = increase();
    };

위의 코드를 보면 counter를 상태관리 하기 위해 전역변수로 선언하여 활용하고 있는데
전역 변수를 사용할 경우 외부에서도 접근이 가능하므로 의도치 않은 동작이 생길 위험이 있습니다.

방금 언급한 문제를 해결한 아래의 코드를 살펴봅시다.


	var incleaseBtn = document.getElementById('inclease');
    var count = document.getElementById('count');

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

      // 클로저로 반환
      return function () {
        return ++counter;
      };
    }());

    incleaseBtn.onclick = function () {
      count.innerHTML = increase();
    };

스크립트가 실행되면 즉시실행함수가 호출되어 라이플 사이클이 종료되고 increase변수에
반환되는 함수가 남습니다. 클로저인 이 함수는 종료된 즉시실행함수의 counter변수를 기억하므로
접근하여 counter변수를 제어할 수 있습니다.

이처럼 클로저를 활용하면 전역 변수의 사용을 억제할 수 있고 이는 의도치 않은 동작을 사전에
방지해줌으로 안전하게 프로그래밍이 가능하게 합니다.

끝으로 클로저를 사용할 때 자주 발생하는 실수를 예제를 통해 살펴보겠습니다.


  var array = [];

  for (var i = 0; i < 5; i++) {
      array[i] = function () {
        return i;
      }
  }

  for (var j = 0; j < array.length; j++) {
      console.log(array[j]());
  }

위의 코드를 보면 array배열에 0~4까지의 i를 반환하는 5개의 함수를 할당했음으로 결과는
0 1 2 3 4가 나올거라고 예상을 하지만 실제로는 4만 나옵니다.

왜 이런 문제가 발생했을까요?

이유는 for문에서 사용하는 변수i는 전역 변수이기 때문이다. 변수 선언 방식 var는
함수 레벨 스코프여서 함수에서 선언되지 않은 변수는 전부 전역 변수로 처리한다.

그렇다면 이러한 문제를 해결한 아래의 코드를 살펴보자


  var array = [];

  // 즉시 실행 함수로 해결
  for (var i = 0; i < 5; i++) {
      array[i] = (function(id){
          return function () {
                return id;
              }
          })(i);
  }

  for (var j = 0; j < array.length; j++) {
      console.log(array[j]());
  } 

  // 변수선언방식 var를 let으로 변경하여 해결
  for (let i = 0; i < 5; i++) {
      array[i] = (function(id){
          return function () {
                return id;
              }
          })(i);
  }

  for (var arr of array) {
      console.log(arr());
  } 

이제 예상한 결과 값이 나옵니다. 즉시실행함수를 이용해 외부 함수를 만들어주고 외부 함수의
매개변수 id에 인자로 i값을 받으면서 i를 내부 함수에서 참조할 수 있는 지역 변수로 바꿔주었습니다.
즉, 위의 코드에서 한 일은 전역 변수 i를 지역 변수로 바꿔준거 하나입니다.
이는 es6문법에서는 for문의 변수 선언 방식을 var에서 let으로 선언함으로 간단히 해결이 됩니다.

profile
아는만큼 보인다

0개의 댓글