top bar

글 목록

2017년 7월 3일 월요일

[Javascript] Closure의 변수 공유 문제

 무언가 난해한 개념으로 이해되는 'lexical scope'(어휘적 유효범위)는, 그냥 '유효범위' 라고 생각해도 상관이 없을 듯 싶다. (이하 유효범위라 하겠다) 자바스크립트는 함수를 실행할때, 해당 함수가 정의되었을 때의 유효범위를 사용하여 실행 한다. 아래의 코드를 보자.

function funcCreator() {
  const x = 10;
  function lexicalScopeFunc() {
    console.log(x);
  };
  return lexicalScopeFunc;
}

funcCreator()();

위의 코드는 'funcCreator' 함수를 실행하여 'lexicalScopeFunc' 가 반환되게 하고, 반환된 함수를 즉각 호출한다.  해당 함수를 호출 하면 console 창에 참조 'x' 의 값인 '10'이 출력된다. 

정작 'lexicalScopeFunc' 함수'만' 본다면, 참조 'x'는 해당 함수의 scope 에서 정의 되었다. 그런데 어떻게 console창에 참조 'x'의 값이 정상적으로 출력이 되는 것일까? 바로 해당 lexicalScopeFunc 함수가 실행될때 해당 함수가 정의 되었던 유효범위, 즉 'funcCreator' 함수의 scope 를 이용하여 실행하기 때문이다. 다시말해서, lexicalScopeFunc 함수는 자신이 선언된 funcCreator 함수 내부의 유효범위와 연결된 채 존재하는 함수인 것이다.

바로 이러한 개념을 '클로져(Closure)' 라고 한다.

이러한 클로져 개념에는 아주 고전적으로 논의되는 문제가 있는데, 바로 '변수 공유' 문제이다. 아래의 코드를 보면 문제가 무엇인지 명확해 진다.

var closureList = [];
for(var i = 0; i < 10; i++) {
  closureList.push(() => {
    console.log(i);
  });
}

for(var idx in closureList) {
    closureList[idx]();
}

closureList 배열안에 삽입한 요소는 각각 클로져 함수인데, 자신이 선언된 유효범위의 'i' 값을 출력한다. 때문에 위의 코드를 실행 했을때 우리는 console창에 0부터 9까지 출력되는 것을 기대할 것이다.

그러나 정작 코드를 실행해 보면 '10' 이 10번 출력된다. 이유를 살펴보자면, closureList 안에 삽입된 각각의 클로져 함수가 해당 유효범위(여기서는 전역범위가 될것이다)의 'i' 변수를 공유하기 때문이다. for문이 종료될때 변수 'i' 는 값이 10이 되고, 각각 클로져는 최종 값이 10인 변수 'i'를 출력하기때문에 그러한 결과가 나오는 것이다.

이를 해결하려면 어떻게 해야할까?

우선은 IIFE(Immediately Invoked Function Expressions)를 사용하여 유효범위를 강제한다. 
아래와 같은 코드이다.

var closureList = [];
for(var i = 0; i < 10; i++) {
  closureList.push(
    (function() {
      var localVar = i;
      return function() {
        console.log(localVar);
      };
    })()
  );
}

for(var i = 0; i < 10; i++) {
    closureList[i]();
}

closureList에 함수 객체를 삽입할때, IIFE를 통해 독립적인 유효범위를 생성하여 그안에서 함수를 반환하여 배열에 삽입한다. 이렇게 되면 IIFE의 유효범위 안에서 'i'는 'localVar' 라는 변수로 바인딩되고, 독립적인 유효범위 안의 변수가 된다.

그러나... 위 코드가 단순한 예제이기에 망정이지, 조금이라도 복잡한 비즈니스 코드로 작성한다면 IIFE는 읽기에 무척 짜증이 날 것 같다.

아주 단순한 해결 방법이 있는데, ECMAScript6 에서 제공하는 'let' 을 이용하여 변수를 선언하는 것이다.
아래의 코드를 다시 보자.

var closureList = [];
for(let i = 0; i < 10; i++) {
  closureList.push(function() {
    console.log(i);
  });
}

for(var idx in closureList) {
    closureList[idx]();
}

차이가 보이는가? closureList 배열에 함수를 삽입하는 for 구문안에서 인덱스를 'let'으로 선언했다. 다들 알겠지만 'var' 선언 방식의 scope는 함수의 유효범위를 사용한다. 하지만 'let'은 대부분의 프로그래밍 언어가 그렇듯이 '블록'의 유효범위이다. 때문에 반복되는 for 구문의 블록이 해당 let 변수의 독립적인 유효범위로 작용하기 때문에 우리가 원하는 0, 1, 2, .. 9 가 출력이 되는 것이다.


댓글 없음:

댓글 쓰기