본문 바로가기
Development

[21.01.11] YDKJSY - Using Closures

by igy95 2024. 1. 7.

See the Closure

클로저는 함수의 동작 혹은 함수 그 자체다. 객체나 클래스는 클로저를 가질 수 없다. 클로저는 하나의 함수로써 그것의 초기 선언 지점으로부터 형성된 스코프 체인의 다른 분기에서 사용된다. 만약 선언된 곳과 같은 스코프에서 실행하는 함수라면, 그것은 클로저가 아니다.

function count() {
      var counter = 0;

    return function() {
        return counter++;
    }
}

var counter1 = count();

counter1(); //0
counter1(); //1
counter1(); //2
counter1(); //3

함수 안에서 내부 함수를 반환하는 어떤 로직을 작성하고 그것을 특정한 변수에 할당하는 식으로 호출을 마치고 나면, 흔히 가비지 컬렉터 때문에'반환된 내부 함수만이 할당된 변수에 보존되어 있으며 확인할 수 있을 것'이라 추측한다. 하지만 이는 생각과 다르게 흘러 간다. 클로저는 내부 함수가 선언된 지점에서의 외부 스코프 변수에 접근할 수 있도록 허용하는 매커니즘을 가지고 있다. (호출로 인해 외부 스코프가 소멸되었더라도 말이다.)

Adding Up Closures

function adder(num1) {
    return function addTo(num2){
        return num1 + num2;
    };
}

var add10To = adder(10);
var add42To = adder(42);

add10To(15);    // 25
add42To(9);     // 51

클로저는 사실 단일 렉시컬 선언보다는 각각의 함수 인스턴스에 더 관련되어 있다. 해당 코드를 살펴 보면, adder 내부에 addTo 라는 함수가 한 번 선언되어 있으니 단일 클로저를 가지고 있을 듯하지만, 외부 함수가 호출될 때마다 addTo 라는 새로운 함수 인스턴스가 생성되는 셈이니 새로운 클로저도 계속 생성된다는 걸 알 수 있다. 이는 클로저가 컴파일 과정의 렉시컬 스코프에 기반하지만, 결국 함수 인스턴스들의 런타임 특성에서 관찰할 수 있다는 점을 시사한다.

Live Link, Not a Snapshot

클로저는 사실 변수 전체에 접근을 유지하는 live link 이다. 그 말은 곧 클로저는 해당 변수의 값에 참조 뿐만 아니라, 변수의 값을 수정할 수 있다는 것이다. 흔히 클로저는 변수가 아닌 변수의 값에 연관되어 있을 것이라는 생각에 개발자들은 종종 실수하고는 하는데, 그와 관련해 다음과 같은 사항을 고려해 볼 수 있다.

var studentName = "Frank";

var greeting = function hello() {
    // we are closing over `studentName`,
    // not "Frank"
    console.log(
        `Hello, ${ studentName }!`
    );
}

// later

studentName = "Suzy";

// later

greeting();
// Hello, Suzy!

위의 예시처럼, greeting 변수에 함수가 할당되는 시점에서 studentName 은 Frank 라는 값을 가지고 있지만 함수가 호출되는 시점에서 studentName 의 값이 변했기 때문에 greeting의 클로저는 값이 아닌 변수에 접근한다는 점에서 결국 새로운 할당 값을 출력한다.

What if I Can't See It?

만약 기술 면접 질문에서 클로저에 관련한 질문이 나온다면, 아마 높은 확률로 클로저의 실제 예시를 요구할 수 있다. 예를 들어 '단순히 함수 안에 함수가 존재하면 클로저인가요?' 라는 식으로 말이다. 함수 안에 함수를 호출하는 방식으로 로직이 있다고 해도 다음과 같은 경우는 클로저라고 할 수 없다.

  • 함수를 호출했을 때 내부 함수의 호출이 같은 스코프 체인 상에서 이미 처리 되는 경우
    -> 내부 함수에서 바깥 스코프의 변수에 접근했다 하더라도, 이것은 단순히 렉시컬 스코프의 개념이지 클로저가 아니다.
  • 함수를 호출했을 때 내부 함수가 바깥 스코프의 변수에 접근하지만, 해당 변수가 전역 스코프에 위치할 경우
    -> 전역 변수는 어디에서나 접근이 가능하기 때문에 이 또한 클로저의 예시라 할 수 없다.
  • (내부 함수의) 외부 스코프에 변수가 존재하지만, 그것에 접근하지 않는 경우
  • 클로저의 특성을 갖추었음에도 불구하고 내부 함수에 대한 호출이 코드 상에 존재하지 않을 경우

Observable Definition

클로저는 함수가 해당 변수에 접근하지 못하는 범위에서 실행이 되는 동안, 외부 범위의 변수를 사용했을 때 관찰된다.

 

위의 정의를 참고하여, 클로저에 관한 전제를 설정할 수 있다.

  • 관련된 함수여야 할 것
  • 외부 스코프로부터 적어도 하나의 변수를 참조할 것
  • 변수들로부터 스코프 체인의 다른 분기에서 사용될 것

The Closure Lifecycle and Garbage Collection (GC)

클로저는 함수 인스턴스에 관련된 매커니즘이라 함수의 참조가 유지되는 동안 변수에 대한 접근이 지속된다. 그래서 클로저는 이미 수행이 완료된 로직에 대한 GC를 예상치 못한 곳에서 막아버릴 수도 있기 때문에, 더이상 필요하지 않은 함수 참조를 적절히 폐기해주는 것 또한 중요하다. 가령, 클로저 패턴으로 이벤트 핸들러를 등록하는 함수를 만들 경우(옵저버 패턴)에는 더이상 이벤트 핸들러가 필요하지 않다면 삭제 시켜서 필요 이상의 메모리를 차지하지 않도록 한다.

Per Variable or Per Scope?

이 부분은 구체적으로 이해하지는 못했지만, 화두를 던진 저자의 의도를 내심 짐작할 수 있었다. 현대의 JS 엔진은 프로그램의 최적화를 위해 필요 없는 변수는 메모리에서 지워버리는 GC 기술을 사용한다. 하지만 클로저의 특성 상, 예상치 못한 곳에서 이미 수행이 끝나 더이상 필요 없는 변수의 메모리를 그대로 유지하고 있을 가능성이 있기 때문에, 단순히 엔진의 기술에 의존하기보다는 클로저에서 참조하는 변수 중 object type (array, object) 처럼 꽤 많은 메모리를 사용하고 계속 유지될 염려가 있는 부분은 자체적으로 null 값을 부여하는 식으로 메모리 낭비를 막아 주자는 얘기를 하고 있다. 때문에 프로그램 내부에서 클로저가 어디서 나타나는지, 그것이 참조하려는 변수가 무엇인지 숙지하는 것은 꽤나 중요한 자세다.

An Alternative Perspective

지금까지 글을 읽으며 클로저에 대한 현재의 관점으로는 '함수가 어디서 사용되던 간에, 클로저는 참조 변수에 접근을 가능하게 하기 위해 기존에 선언된 original scope에 대한 링크 (live link)를 유지하고 있다'고 말할 수 있다. 하지만 클로저에 대해 다른 관점으로도 생각해볼 수 있는데, 이 대안적인 모델은 '일급 객체'로써의 함수 성질을 강조하는 대신에 함수가 어떻게 레퍼런스로 유지되고 레퍼런스-카피로 할당되는지를 살펴 본다.

 

전자는 내부 함수의 인스턴스가 다른 스코프의 어떤 변수에 할당되었을 때, 새로운 인스턴스의 생성으로 그 위치가 실제로 이동한다고 보고 있지만, 후자(대안 모델)의 경우 내부 함수의 인스턴스는 여전히 제자리에 머물고 온전한 스코프 체인을 유지하고 있다고 본다. 때문에 다른 스코프의 변수에 할당되었을 때, 내부 함수 인스턴스에 대한 참조가 이루어진다는 것이다(value vs reference). 그래서 다른 스코프에서 넘겨 받는 것은 인스턴스에 대한 레퍼런스지, 인스턴스 그 자체가 아니다.

 

사실 클로저에 대해 두 가지 관점 중 아무거나 차용해도 설명과 결과에 대한 예측이 가능하지만, 저자는 reference 와 in-place instance 의 개념을 도입하여 좀 더 단순화할 수 있도록 새로운 mental model을 제시했다. 어떤 관점을 취할 지는 본인의 몫이다.

Closer to Closure

이번 챕터에서 다룬 클로저를 요약하면 이렇다.

  • 실측적인 관점; 클로저는 그 외부의 변수들을 기억하는 함수 인스턴스이고 다른 스코프에서 통과되거나 사용된다.
  • 실행적인 관점; 클로저는 함수 인스턴스이지만 그것에 참조하는 동안 스코프 환경은 제자리에서 유지되며 다른 스코프에서 통과되거나 사용된다.

클로저를 이용하면 좋은 점은?

  • 클로저는 매번 연산을 행해야하는 로직 대신에 이전에 결정된 정보를 함수 인스턴스에 기억 시킴으로써 효율성을 향상시킬 수 있다.
  • 클로저는 미래에 사용할 변수들의 정보를 접근 가능하게 보장해주면서, 함수 인스턴스 안에 변수들을 캡슐화하며 스코프 노출을 최소화하기 때문에 코드의 가독성을 높인다. 보장된 정보는 모든 실행에 대해 따로 넘겨줄 필요가 없기 때문에, 함수가 상호작용을 하는 것이 더 명료해진다.