TIL69.Closure

조연정·2021년 1월 19일
0
post-thumbnail

함수형 프로그래밍 언어에 등장하는 보편적인 특성인 클로저에 대해 알아보자.

closure란?

MDN의 정의에 따르면, 클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다. 흔히 함수 내에서 함수를 정의하고 사용하면 클로저라고들 부른다.
클로저는 어떠한 내부함수를 감싸는 외부함수가 실행되고나서 종료되었다고 하더라도 내부함수에서 외부함수의 변수에 접근할 수 있는 방법이다. 간단히 말하면 클로저는 자신이 생성될 때의 환경(Lexical environment)을 기억하는 함수다라고 말할 수 있다.

*렉시컬 환경: 스코프는 함수를 호출할 때가 아니라 함수를 어디에 선언하였는지에 따라 결정되는데 이를 렉시컬 스코핑(Lexical scoping)라 한다.

function buildName(name) {
	let greeting = "Hello" + name;
    return greeting;
}

함수가 호출될 때마다 새로운 스코프가 생겨나고, 함수 안에 선언된 지역변수는 그 스코프를 따르게 된다. 함수실행이 끝나면, 보통 스코프는 종료된다.
위의 함수를 보면 'greeting'이라는 지역변수를 선언하고, 이를 리턴한다. 함수의 실행이 끝나면 스코프를 다시 불러올 수 없다.

외부 함수의 변수를 참조하는 내부 함수

var outer = function () {
  var a = 1;
  var inner = function() {
    return ++a;
  };
 return inner();
};

var outer2 = outer();
console.log(outer2); //2

inner함수 내부에 변수 a를 선언하지 않았기 때문에 상위 컨텍스트인 outer의 렉시컬 환경에 접근해서 a를 찾는다. a++인 2가 출력되고 outer함수의 실행 컨텍스트가 종료되면 렉시컬 환경에 저장된 식별자들(a, inner)에 대한 참조를 지운다. 각 주소에 저장되어 있던 값들은 자신을 참조하는 변수가 하나도 없게되므로 가비지 콜렉터의 수집대상이 된다. outer함수 종료 이전에 inner함수의 실행 컨텍스트가 종료돼있기때문에 이후에 별도로 inner함수를 호출할 수 없다.

var outer = function () {
  var a = 1;
  var inner = function() {
    return ++a;
  };
 return inner;
};

var outer2 = outer();
console.log(outer2); //2
console.log(outer2); //3

1번과 달리 2번 코드에서는 inner함수의 실행 결과가 아닌 inner함수 자체를 반환했다. inner함수의 실행시점에서 outer함수는 이미 실행이 종료된 상태인데 outer함수의 렉시컬환경에 접근이 가능하다. 가비지 컬렉터는 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는 특성이 있다. outer함수는 실행 종료 시점에 inner함수를 반환한다. 외부함수인 outer의 실행이 종료되더라고 내부 함수인 inner함수가 언젠가 outer2를 실행함으로써 호출될 가능성이 열린 것으로 볼 수 있다.

클로저와 메모리 관리

메모리 누수의 위험을 이유로 클로저 사용을 조심해야 한다거나 심지어 지양해야 한다고 주장하는 사람들도 있지만 메모리 소모는 클로저의 본질적인 특성일 뿐이다. 오히려 이러한 특성을 정확히 이해하고 잘 활용하도록 노력해야한다. '메모리 누수'라는 표현은 개발자의 의도와 달리 어떤 값의 참조 카운트가 0이 되지 않아 가비지 컬렉터의 수거 대상이 되지 않는 경우에는 맞는 표현이지만 개발자가 의도적으로 참조 카운트를 0이 되지 않게 설계한 경우는 '누수'라고 할 수 없다.

클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생한다. 그 필요성이 사라진 시점에는 메모리를 소모하지 않게 해주면 된다. 참조 카운트를 0으로 만들면 언젠가 가비지 컬렉터가 수거해갈 것이고, 소모됐던 메모리는 회수된다. 이때 참조 카운트를 0으로 만드는 방법은 식별자에 참조형이 아닌 기본형 데이터(보통 null, undefined)를 할당해주면 된다.

 1 var outer = (function (){
2   var a = 1;
3   var inner = function() {
4        return ++a;
5      };
6   return inner;
7 })();
8
9   console.log(outer());  
10  console.log(outer());
11  outer = null;           // outer 식별자의 inner 함수 참조를 끊음

cloure문제가 잘 발생하는 환경

반복문비동기코드가 함께 쓰일경우 클로저문제가 발생할 확률이 높다.

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

0부터 10까지 차례대로 나오는 결과를 원했지만, 실제로는 10이 11번 나오는 결과가 된다.
컴퓨터는 계산속도가 빠르기 때문에 이미 반복문으로 10번을 다 돌고 i는 10번째가 되고 빠져나온 뒤, i가 10인 상태에서 setTimeout이 실행된다.

  • 비동기함수 setTimeout은 stack이 비워진 상태에서 실행이 된다. for문이 전부 돌아간 다음에 stack이 전부 비워지면 그때 setTimeout이 실행이 되는데 i의 최종호출 값은 10이니까 10이 10번 출력된다.
  • 9가아니고 10인 이유는? => for문은 9까지 돌고 중단되고, 10이된 i가 for문 밖으로 나가게 된다. 그제서야 setTimeout이 실행되기 때문에 i가 10이 된다.

=> 변수 var가 아닌 let을 사용하면, 원하는 결과를 도출할 수 있다.
for문이 9에서 종료되고 나가면, 블록스코프인 let은 참조할 수 있는 함수가 없어지기 때문에 클로저 실수를 해결할 수 있다.

클로저 활용 사례

콜백 함수와 클로저

  • 콜백 함수를 내부 함수로 선언해서 외부 변수를 직접 참조하는 방법으로 클로저를 사용하는 방법
  • bind 메서드로 값을 직접 넘겨줘서 클로저를 발생 시키지 않는 대신 여러가지 제약이 있는 방법
  • 콜백 함수를 고차함수로 바꿔서 클로저를 적극적으로 활용한 방법

접근 권한 제어

var outer = function () {
  var a = 1 
  var inner = function () {
    return ++a
  }
  return inner
}
var outerFunc = outer()
console.log(outerFunc())

outer 함수를 종료할 때 inner 함수를 반환함으로써 outer 함수의 지역변수인 a의 값을 외부에서도 읽을 수 있게 되었다.
외부에서는 outer라는 변수를 통해 outer함수를 실행 할 수는 있지만 outer함수 내부에는 어떤한 개입도 할 수 없다.
즉, 외부에서는 오직 outer함수가 return한 정보에만 접근 할수 있는 것이고, 내부에서만 사용하는 정보들은 return 하지 않는 것으로 접근 권한 제어가 가능하다. return한 변수들은 공개맴버(public member)가 되고, 그렇지 않는 변수들은 비공개 멤버(private member)가 되는 것이다.

profile
Lv.1🌷

0개의 댓글