Closure

About

클로저는 자신이 생성될 "때"의 스코프에서 알 수 있었던 변수를 기억하는 함수이다. (클로저가 기억하는 변수의 값은 변할 수 있다.)

  • 스코프: 변수를 어디에서 어떻게 찾을지를 정한 규칙.

    • 함수는 변수 참조 범위를 결정하는 중요한 기준이다. 함수가 중첩되어 있다면 스코프도 중첩되어 생겨난다.

    • 글로벌 스코프를 제외한 외부 스코프에 있었던 변수 중, 클로저 혹은 다른 누군가가 참조하고 있지 않은 모든 변수는 실행 컨텍스트가 끝난 후 가비지 컬렉션 대상이 된다. 어떤 함수가 외부 스코프의 변수를 사용하지 않았고, 그래서 외부 스코프의 환경이 GC의 대상이 된다면, 그렇게 내버려 두는 함수를 클로저라고 보긴 어렵다.

function parent() {
  var a = 5
  function myfn() {
    console.log(a)
  }
  // ...
}

function parent2() {
  var a = 5
  function parent1() {
    function myfn() {
      console.log(a)
    }
    // ...
  }
  // ...
}

예시

다음 f1은:

  • 클로저처럼 외부 변수를 참조하여 결과를 만든다.

  • 상위 스코프의 변수를 사용하고 있다.

하지만 f1은 클로저가 아니다.

글로벌 스코프에서 선언된 모든 변수는 그 변수를 사용하는 함수가 있는지 없는지와 관계 없이 유지된다. a, b 변수가 f1에 의해 사라지지 못하는 상황이 아니므로 f1은 클로저가 아니다.

var a = 10
var b = 20
function f1() {
  return a + b
}
f1() // 30

다음은 함수 안에서 함수를 리턴하는 방식이다. 하지만 f3, f4 모두 클로저가 아니다.

  • f3f2 안에서 생성되었다.

  • f3 바로 위에는 a, b라는 지역 변수도 있다. 하지만 f3 안에서 실제로 사용하고 있는 변수는 c, d이고, 두 변수 모두 f3에서 정의되었다.

  • 자신이 생성될 때의 스코프가 알고 있는 변수 a, b는 사용하지 않았으므로 f3이 기억해야 할 변수는 하나도 없다.

function f2() {
  var a = 10
  var b = 20
  function f3(c, d) {
    return c + d
  }
  return f3
}

var f4 = f2()
f4(5, 7) // 12

다음 예시에서 클로저가 있을까? 정확한 표현은 '있었다'이다.

f4가 실행되고 a, b가 할당된 후 f5가 정의된다. 그리고 f5에서는 a, b가 사용되고 있으므로 f5는 자신이 생성된 환경을 기억하는 클로저가 된다.

하지만 f4의 마지막 라인에서 f5를 실행해 리턴하고 있다.

결국 f5를 참조하고 있는 곳이 어디에도 없으므로 f5는 사라지고, f5가 사라지면 a, b도 사라질 수 있기에 클로저는 f4가 실행되는 사이에 생겼다가 사라진다.

function f4() {
  var a = 10
  var b = 20
  function f5() {
    return a + b
  }
  return f5()
}

다음 f7은 진짜 클로저이다.

  • f7a를 사용하기에 a를 기억해야 하므로 a는 사라지지 않는다.

  • f7f8에 담겼다.

원래대로라면 f6의 지역 변수는 모두 사라져야 하지만 f6 실행이 끝났어도 f7a를 기억하는 클로저가 되었기 때문에 a는 사라지지 않으며, f8을 실행할 때마다 새로운 변수인 b와 함께 사용되어 결과를 만든다.

function f6() {
  var a = 10
  function f7(b) {
    return a + b
  }
  return f7
}

var f8 = f6()
f8(20) // 30
f8(10) // 20

참고로 a의 메모리가 해제되지 않는 것은 개발자가 의도한 것이므로 메모리 누수라고 볼 수 없다.

다음 예시는 오류가 나지 않는다.

  • 클로저는 자신이 생성될 '때'의 스코프에서 알 수 있었던 변수를 기억하는 함수라고 했는데, 여기서 '때'는 생각보다 조금 길다. 그걸 보여주는 예시이다.

    • '때가 조금 길다'에서 말하는 '때'는 함수가 생성이 되는 라인이나 그 이전을 의미하는 것이 아니라, 그 스코프가 실행되고 있는 컨텍스트 전체를 말하기 때문이다. (이 안에서 비동기가 일어나면 더욱 길어지기도 할 것이다.)

  • '스코프에서 알 수 있었던'이라고 하는 부분도 이 예제에서 설명된다.

    • 여기서 말하는 그 스코프는 함수일 수도 있다. 만일 함수라면 함수가 실행될 때마다 그 스코프의 환경은 새로 만들어진다.

    • 클로저 자신이 생성될 '때의 스코프에서 알 수 있었던'의 의미는 '클로저가 생성되고 있는 이번 실행 컨텍스트에서 알 수 있었던'이라는 의미다.

f10에는 익명 함수를 담았다. (물론 function f10(c) { ... }로 해도 정상 동작한다.)

f10이 생성되기 이전에 b는 20으로 초기화 되지 않았다. 클로저는 자신이 생성되는 스코프의 모든 라인, 어느 곳에서 선언된 변수든지 참조하고 기억할 수 있다. 그리고 그것은 변수이기에 클로저 생성 이후 언제든 값이 변경될 수 있다.

function f9() {
  var a = 10
  var f10 = function(c) {
    return a + b + c
  }
  var b = 20
  return f10
}

var f11 = f9()
f11(30) // 60

다시 클로저를 풀어서 정의해 보자.

클로저는 자신이 생성되는 스코프의 실행 컨텍스트에서 만들어졌거나 알 수 있었던 변수 중 언젠가 자신이 실행될 때 사용할 변수들만 기억하는 함수이다. 클로저가 기억하는 변수의 값은 언제든지 남이나 자신에 의해 변경될 수 있다.

실용 사례

클로저의 강력함이나 실용성은 은닉에 있지 않다. 은닉은 의미 있는 기술이자 개념이지만, 은닉 자체가 클로저의 목적은 아니다.

클로저가 강력하고 실용적인 상황은 따로 있다.

  • 이전 상황을 나중에 일어날 상황과 이어 나갈 때

  • 함수로 함수를 만들거나 부분 적용을 할 때

이전 상황을 나중에 일어날 상황과 이어 나갈 때

  • 이벤트 리스너로 함수를 넘기기 이전에 알 수 있었던 상황들을 변수에 담아 클로저로 만들어 기억해 두면, 이벤트가 발생되어 클로저가 실행되었을 때 기억해 두었던 변수들로 이전 상황들을 이어갈 수 있다.

  • 콜백 패턴에서도 마찬가지로 콜백으로 함수를 넘기기 이전 상황을 클로저로 만들어 기억해 두는 식으로 이전의 상황을 이어 갈 수 있다.

정리

클로저는...

  • 자신이 생성될 "때"의 스코프에서 알 수 있었던 변수를 기억하는 함수이다.

  • 사실 사용하기 별로 어렵지 않다.

  • 절차지향, 객체지향, 함수형 프로그래밍 모두를 지탱하는 매우 중요한 기능이자 개념이다.

  • 서로 분리된 컨텍스트나 객체를 쉬운 개념으로 이어준다.

  • 많이 사용하자. 문제가 일어나면 그 때 해결하자. (웬만해서는 문제를 일으키지 않을 것이다.)

REF

Last updated