본문 바로가기
JAVASCRIPT

[JS][스코프 클로저] 클로저 개요

by KBS 2022. 2. 18.
728x90

스포크 클로저

이제 자바스크립트에서 굉장히 중요하지만, 자주 잊어버리곤 해서 거의 신화적인 부분인 클로저를 보자. 렉시컬 스코프에 대한 설명을 제대로 이해했다면 클로저는 앞에서 배웠던 것들보다 쉽고 뻔해 보일 것이다.

깨달음

자바스크립트를 사용해봤지만 단 한번도 클로저 개념을 완전히 이해한 적이 없는 이들에게는 클로저가 열반에 드는 것처럼 고된 노력을 들여야 이해할 수 있는 것일지도 모르겠다.

클로저는 렉시컬 스코프에 의존해 코드를 작성한 결과로 그냥 발생한다. 이용하려고 굳이 의도적으로 클로저를 생성할 필요도 없다. 모든 코드에서 클로저는 생성되고 사용된다. 그러므로 여기서 적절히 클로저의 전반을 파악하면 클로저를 목적에 따라 확인하고, 받아들이고, 이용할 수 있다.

깨달음의 순간이 이럴 것이다. "아. 클로저는 내 코드 전반에서 이미 일어나고 있구나! 숨쉬듯 사용했구나! 난 클로저를 볼 수 있어" 클로저를 이해하는 것은 네오(매트릭스 주인공)가 매트릭스를 처음 봤을 때와 같을 것이다.

핵심

좋다, 이제 과장법과 같잖은 영화 인용은 그만하자. 클로저를 이해하고 파악해야 할 특징 위주로 가감 없이 정의해보자.

클로저는 함수가 속한 렉시컬 스코프를 기억하여 함수가 렉시컬 스코프 밖에서 실행될 때에도 이 스코프에 접근할 수 있게 하는 기능을 뜻한다.

코드를 보면서 앞의 정의가 설명한 바를 살펴보자.

function foo() {
  var a = 2;
  function bar() {
    console.log(a); // 2
  }

  bar();
}

foo();

 

앞의 코드는 중첩 스코프를 다룰 때 보았던 예제와 비슷하다. 함수 bar()는 렉시컬 스코프 검색 규칙을 통해 바깥 스코프의 변수 a에 접근할 수 있다.(이 경우 RHS 참조 검색이다.)

이것이 '클로저'인가? 음, 기술적으로 보자면 그렇게 볼 수 있다. 그러나 앞에서 말한 정의에 따르자면 꼭 그겋지는 않다. a를 참조하는 bar()를 설명하는 가장 정확한 방식은 렉시컬 스코프 검색 규칙에 따라 설명하는 것이고, 이 규칙은 클로저의 일부일 뿐이다.

순전히 학술적인 관점에서 앞의 코드에 대해 말하면 함수bar()foo() 스코프에서 닫힌다. 왜일까? bar()는 중첩되어 foo()안에 존재하기 때문이다. 이 얼마나 쉽고 단순한가.

그러나 이런 방식으로 정의된 클로저는 바로 알아보기 힘들고 앞의 코드에서 클로저가 작동하는 방식을 볼 수도 없다. 렉시컬 스코프는 분명하게 볼 수 있지만, 클로저는 여전히 코드 뒤에 숨겨진 불가사의한 음모의 그림자로 남아있다.

그럼 클로저의 정체를 완전히 들어낼 코드를 살펴보자.

function foo() {
  var a = 2;
  function bar() {
    console.log(a);
  }

  return bar;
}

var baz = foo();

baz();

 

함수 bar()foo()의 렉시컬 스코프에 접근할 수 있고, bar() 함수 자체를 값으로 넘긴다. 이 코드는 bar를 참조하는 함수 객체 자체를 반환한다.

foo()를 실행하여 반환한 값을 baz라 불리는 변수에 대입하고 실제로는 baz() 함수를 호출했다. 이는 당연하게도 그저 다른 확인자 참조로 내부 함수인 bar()를 호출한 것이다. bar()는 의심할 여지없이 실행된다. 그러나 이 경우에 함수 bar는 함수가 선언된 렉시컬 스코프 밖에서 실행됐다.

일반적으로 foo()가 실행된 후에는 foo()의 내부 스코프가 사라졌다고 생각할 것이다. 이것은 엔진이 가비지 콜렉터를 고용해 더는 사용하지 않는 메모리를 해제시킨다는 사실을 알기 때문이다. 더는 foo()의 내용을 사용하지 않는 상황이라면 사라졌다고 보는 게 자연스럽다.

그러나 클로저의 '마법'이 이를 내버려두지 않는다. 사실 foo의 내부 스코프는 여전히 '사용 중'이므로 해제되지 않는다. 그럼 누가 그 스코프를 사용 중인다.? 바로 bar() 자신이다. 선언된 위치 덕에 bar()foo() 스코프에 대한 렉시컬 스코프 클로저를 가지고, foo()bar()가 나중에 참조할 수 있도록 스코프를 살려둔다. 즉, bar()는 여전히 해당 스코프에 대한 참조를 가지는데, 그 참조를 바로 클로저라 부른다.

foo() 선언이 끝나고 수 밀리 초 후 변수 baz를 호출할 대, 해당 함수는 원래 코드의 렉시컬 스코프에 접근할 수 있고 예상한 것처럼 이는 함수가 변수 a에 접근할 수 있다는 것이다.

함수는 원래 코드의 렉시컬 스코프에서 완전히 벗어나 호출됐다. 클로저는 호출된 함수가 원래 선언된 렉시컬 스코프에 계속해서 접근할 수 있도록 허용한다. 물론, 어떤 방식이든 함수를 값으로 넘겨 다른 위치에서 호출하는 행위는 모두 클로저가 작용한 예다.

function foo() {
  var a = 2;
  function baz() {
    console.log(a); // 2
  }

  bar(baz);
}

function bar(fn) {
  fn();
}

 

코드에서 함수 baz를 bar에 넘기고, 이제 fn이라 명명된 함수를 호출했다. 이때 foo()의 내부 스코프에 대한 fn의 클로저는 변수 a에 접근할 때 확인할 수 있다. 이런 함수 넘기기는 간접 적인 방식으로도 가능하다.

var fn;

function foo() {
  var a = 2;
  function baz() {
    console.log(a);
  }

  fn = baz;
}

function bar() {
  fn();
}

foo();

bar(); // 2

어떤 방식으로 내부 함수를 자신이 속한 렉시컬 스코프 밖으로 수송하든 함수는 처음 선언된 곳의 스코프에 대한 참조를 유지한다. 즉, 어디에서 해당 함수를 실행하든 클로저가 작용한다.

이제 나는 볼 수 있다.

앞에서 본 코드들은 클로저 사용법을 보여주기 위해 다소 학술적이고 인위적으로 작성했다. 나는 클로저가 단순히 멋진 새 장난감 이상의 것이라고 말했다. 또 클로저가 모든 코드 안에 존재하는 무언가라고도 이야기했다. 이제 왜 그것이 사실인지 살펴보자.

function wait(message) {
  setTimeout(function timer() {
    console.log(message);
  }, 1000);
}

wait("Hello, closure!!");

 

내부 함수 timer를 setTimeout()에 인자로 넘겼다. timer 함수는 wait() 함수의 스코프에 대한 스코프 클로저를 가지고 있으므로 변수 message에 대한 참조를 유지하고 사용할 수 있다.

wait()실행 1초후, wait의 내부 스코프는 사라져야 하지만 익명의 함수가 여전히 해당 스코프에 대한 클로저를 가지고 있다.

엔진 내부 깊숙한 곳의 내장 함수 setTimeout()에는 아마도 fn이나 func 정도로 불릴 인자의 참조가 존재한다. 엔진은 해당 함수 참조를 호출하여 내장 함수 timer를 호출하므로 timer의 렉시컬 스코프는 여전히 온전하게 남아 있다.

클로저

제이쿼리 신봉자라면 다음을 보자.

function setupBot(name, selector) {
  $(selector).click(function activator() {
    console.log("Activating" + name);
  });
}

setupBot("Closure Bot 1", "#bot_1");
setupBot("Closure Bot 2", "#bot_2");

자체의 렉시컬 스코프에 접근할 수 있는 함수를 인자로 넘길 대 그 함수가 클로저를 사용하는 것을 볼 수 있다. 타이머, 이벤트 처리기, Ajax요청, 윈도 간통신, 웹 워커와 같은 비동기적 작업을 하며 콜백 함수를 넘기면 클로저를 사용할 준비가 된 것이다.

var a = 2;
(function IIFE() {
  console.log(a);
})();

 

이 코드는 '작동'하지만 엄격히 말해 클로저가 사용된 것은 아니다. 'IIFE' 함수가 자신의 렉시컬 스코프 밖에서 실행된 것이 아니기 때문이다. IIFE 함수는 선언된 바로 그 스코프 안에서 호출됐다. 변수 a는 클로저가 아니라 일반적인 렉시컬 스코프 검색을 통해 가져왔다.

클로저는 기술적으로 보면 선언할 때 발생하지만, 바로 관찰할 수 있는 것은 아니다. 누군가 말했던것처럼 아무도 듣는 사람 없이 쓰러진 숲 속 나무와 같다.

IIFE 자체는 클로저의 사례가 아니지만 IIFE는 틀림없이 스코프를 생성하고 클로저를 사용할 수 있는 스코프를 만드는 가장 흔한 도구의 하나다. 따라서 IIFE 자체가 클로저를 작동시키지는 않아도 확실히 클로저와 연관 깊다.

 

"숲속에서 나무가 쓰러지는데, 아무도 그 소리를 듣지 못한다면 쓰러지는 나무가 소리를 낸다고 말할 수 있는가? 존재하는 것은 지각되는 것이다" - 철학자 비숏 조지 버클리

 


참고

  • You Don't Know JS - 타입과 문법, 스코프와 클로저( 한빛 미디어 )

 

728x90

댓글