[JavaScript] 클로저 (Closure)

Moonlog·2020년 12월 17일
0

JavaScript

목록 보기
1/1
post-thumbnail

클로저 (Closure)란?

MDN에서는 클로저를 다음과 같이 정의하고 있다.

클로저는 독립적인 (자유) 변수를 가리키는 함수이다. 또는, 클로저 안에 정의된 함수는 만들어진 환경을 ‘기억한다’.

흔히 함수 내에서 함수를 정의하고 사용하면 클로저라고 한다. 하지만 대개는 정의한 함수를 리턴하고 사용은 바깥에서 하게된다. 아래 코드를 보자.

function getClosure() {
  let text = 'Hello World!';
  function closure() {
    return console.log(text);
  };
  return closure;	// 익명함수로 return function() {}; 이런 식으로 작성해 줄 수 있다.
}
>
const myFunc = getClosure();
myFunc(); // 'Hello World!'

위에서 getClosure()는 함수를 반환하고, 반환된 함수는 getClosure() 내부에서 선언된 변수를 참조하고 있다. 또한 이렇게 참조된 변수는 함수 실행이 끝났다고 해서 사라지지 않았고, 여전히 제대로 된 값을 반환하고 있는 걸 알 수 있다.

여기서 반환된 함수가 클로저인데, MDN에서 정의된 내용에서도 말했듯 환경을 기억하고 있는 것처럼 보인다. 아직은 잘 이해가 되지 않으니, 다른 예제도 한 번 보겠다.

let globalVariable = 100;

function externalFunc(num = 0) { // num = 0은 넘겨받은 파라미터가 없을 시 num을 0으로 초기화.
  let localVariable = num;
  return function(x = 0) {	
    return console.log(globalVariable+localVariable+x);
  };
}

const func1 = externalFunc(50);
const func2 = externalFunc(100);
const func3 = externalFunc(200);

func1(50);  // 200
func2();    // 200

globalVariable = 1000;
func3(300); // 1500

출력된 결과를 보면 localVariable 변수가 동적으로 변화하고 있는 것처럼 보인다. 실제로는 localVariable라는 변수 자체가 여러 번 생성된 것이다. 즉, func1()과 func2(), func3()은 서로 다른 환경을 가지고 있다.

클로저를 이용해서 프라이빗 메소드(private method) 흉내내기

자바와 같은 몇몇 언어들은 메소드를 프라이빗으로 선언할 수 있는 기능을 제공한다. 이는 같은 클래스 내부의 다른 메소드에서만 그 메소드들을 호출할 수 있다는 의미이다.

자바스크립트는 태생적으로는 이런 방법을 제공하지 않지만 클로저를 이용하여 프라이빗 메소드를 흉내내는 것이 가능하다. 프라이빗 메소드는 코드에 제한적인 접근만을 허용한다는 점 뿐만 아니라 전역 네임 스페이스를 관리하는 강력한 방법을 제공하여 불필요한 메소드가 공용 인터페이스를 혼란스럽게 만들지 않도록 한다.

아래 코드는 프라이빗 함수와 변수에 접근하는 퍼블릭 함수를 정의하기 위해 클로저를 사용하는 방법을 보여준다. 이렇게 클로저를 사용하는 것을 모듈 패턴이라 한다.

let counter = (function() {	// * IIFE - (function(){})();
  let privateCounter = 0;	 
  function changeBy(val) {	
    privateCounter += val;	
  }				
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };
})();

console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1

counter.increment, counter.decrement, counter.value 세 함수에 의해 공유되는 하나의 어휘적(lexical) 환경을 만든다.

공유되는 어휘적 환경은 실행되는 익명 함수 안에서 만들어진다. 이 익명 함수는 정의되는 즉시 실행된다. 이 어휘적 환경은 두 개의 프라이빗 아이템을 포함한다. 하나는 privateCounter라는 변수이고 나머지 하나는 changeBy라는 함수이다. 둘 다 익명 함수 외부에서 접근될 수 없다. 대신에 익명 래퍼에서 반환된 세 개의 퍼블릭 함수를 통해서만 접근되어야만 한다.

위의 세 가지 퍼블릭 함수는 같은 환경을 공유하는 클로저다. 자바스크립트의 어휘적 유효 범위 덕분에 세 함수 각각 privateCounter 변수와 changeBy 함수에 접근할 수 있다.

이 함수를 별도의 변수 makeCounter 저장하고 이 변수를 이용해 여러 개의 카운터를 만들 수 있다.

let makeCounter = function() {
  let privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
};

let counter1 = makeCounter();
let counter2 = makeCounter();

console.log(counter1.value()); // logs 0
counter1.increment();
counter1.increment();
console.log(counter1.value()); // logs 2
counter1.decrement();
console.log(counter1.value()); // logs 1
console.log(counter2.value()); // logs 0

두 개의 카운터가 어떻게 다른 카운터와 독립성을 유지하는지 주목해보자. 각 클로저는 그들 고유의 클로저를 통한 privateCounter 변수의 다른 버전을 참조한다. 각 카운터가 호출될 때마다 하나의 클로저에서 변수 값을 변경해도 다른 클로저의 값에는 영향을 주지 않는다.

이런 방식으로 클로저를 사용하여 객체지향 프로그래밍의 정보 은닉과 캡슐화 같은 이점들을 얻을 수 있다.

루프에서 클로저 생성하기

for (let i = 0; i < 10; i++) {
  setTimeout(function() {
    console.log(i);
  }, 100);
}

간단하게 0-9까지의 정수를 출력하는 코드이지만 실제로 돌려보면 엉뚱하게도 10만 열 번 출력되는 걸 볼 수 있다. 왜일까?

먼저 setTimeout()에 인자로 넘긴 익명함수는 모두 0.1초 뒤에 호출될 것이다. 그 0.1초 동안에 이미 반복문이 모두 순회되면서 i값은 이미 10이 된 상태. 그 때 익명함수가 호출되면서 이미 10이 되어버린 i를 참조하는 것이다.

이 경우에도 클로저를 사용하면 원하는 대로 동작하도록 만들 수 있다.

for (let i = 0; i < 10; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j);
    }, 100);
  })(i);
}

중간에 IIFE를 덧붙여 setTimeout()에 걸린 익명함수를 클로저로 만들었다. 앞서 말한대로 클로저는 만들어진 환경을 기억한다. 이 코드에서 i는 IIFE내에 j라는 형태로 주입되고, 클로저에 의해 각기 다른 환경속에 포함된다. 반복문은 10회 반복되므로 10개의 환경이 생길 것이고, 10개의 서로 다른 환경에 10개의 서로 다른 j가 생긴다.

클로저의 성능

클로저는 각자의 환경을 가진다. 이 환경을 기억하기 위해서는 당연히 메모리가 소모될 것이다. 클로저를 생성해놓고 참조를 제거하지 않는 것은 C++에서 동적할당으로 객체를 생성해놓고 delete를 사용하지 않는 것과 비슷하다. 클로저를 통해 내부 변수를 참조하는 동안에는 내부 변수가 차지하는 메모리를 GC가 회수하지 않는다. 따라서 클로저 사용이 끝나면 참조를 제거하는 것이 좋다.

let globalVariable = 100;

function externalFunc(num = 0) { 
  let localVariable = num;
  return function(x = 0) {	
    return console.log(globalVariable+localVariable+x);
  };
}

let func1 = externalFunc(50);
let func2 = externalFunc(100);
let func3 = externalFunc(200);

func1(50);  // 200
func2();    // 200

globalVariable = 1000;
func3(300); // 1500

// 여기서 메모리를 release 시키기 위해 클로저의 참조를 제거해야 한다.
func1 = null;
func2 = null;
func3 = null;

즉시 실행 함수 표현(IIFE, Immediately Invoked Function Expression)은 정의되자마자 즉시 실행되는 Javascript Function 를 말한다.

(function () { statements })();

이는 Self-Executing Anonymous Function 으로 알려진 디자인 패턴이고 크게 두 부분으로 구성된다. 첫 번째는 괄호((), Grouping Operator)로 둘러싸인 익명함수(Anonymous Function)이다. 이는 전역 스코프에 불필요한 변수를 추가해서 오염시키는 것을 방지할 수 있을 뿐 아니라 IIFE 내부안으로 다른 변수들이 접근하는 것을 막을 수 있는 방법이다.

두 번째 부분은 즉시 실행 함수를 생성하는 괄호()이다. 이를 통해 자바스크립트 엔진은 함수를 즉시 해석해서 실행한다.


* 참고

profile
Start 20.12.10 ~ ing

0개의 댓글