자바스크립트 클로저(Closure)

Jemin·2023년 6월 12일
0

프론트엔드

목록 보기
17/51
post-thumbnail

클로저란?

클로저(Closure)는 자바스크립트에서 매우 중요하고 강력한 개념이다. 클로저는 함수와 그 함수가 선언된 렉시컬 환경(Lexical Enviroment)의 조합이다. 클로저는 함수가 렉시컬 스코프(Lexical Scope)에서 선언될 때 생성된다.

자바스크립트에서 함수는 다른 함수 내부에서 정의될 수 있다. 이때 내부 함수는 외부 함수의 변수와 매개변수에 접근할 수 있다. 이러한 내부 함수는 외부 함수의 스코프 체인(Scope Chain)을 통해 접근할 수 있게 된다.

클로저는 이러한 내부 함수와 그 함수가 접근할 수 있는 변수들의 조합이다. 내부 함수가 클로저를 형성하면 외부 함수의 변수들은 계속해서 메모리에 남아 있게 되어 내부 함수가 이 변수들에 접근할 수 있게 된다. 이렇게 클로저는 함수가 종료된 후에도 해당 함수의 변수들을 계속해서 참조할 수 있도록 한다.

렉시컬 스코핑(Lexical Scoping)

클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical Scoping)을 먼저 이해해야 한다.

function outerFunc() {
  var x = 10;
  var innerFunc = function () { console.log(x); };
  innerFunc();
}

outerFunc(); // 10

함수 outerFunc 내에서 내부함수 innerFunc 이 선언되고 호출되었다. 이때 내부함수 innerFunc 은 자신을 포함하고 있는 외부함수 outerFunc의 변수 x에 접근할 수 있다. 이는 함수 innerFunc 이 함수 outerFunc의 내부에 선언되었기 때문이다.

스코프는 함수를 호출할 때가 아니라 함수를 어디에 선언하였는지에 따라 결정된다. 이를 렉시컬 스코핑(Lexical scoping)라 한다. 위 예제의 함수 innerFunc은 함수 outerFunc의 내부에서 선언되었기 때문에 함수 innerFunc의 상위 스코프는 함수 outerFunc이다. 함수 innerFunc이 전역에 선언되었다면 함수 innerFunc의 상위 스코프는 전역 스코프가 된다.

함수 innerFunc이 함수 outerFunc의 내부에 선언된 내부함수이므로 함수 innerFunc은 자신이 속한 렉시컬 스코프(전역, 함수 outerFunc, 자신의 스코프)를 참조할 수 있다.

내부함수 innerFunc이 자신을 포함하고 있는 외부함수 outerFunc의 변수 x에 접근할 수 있는 것, 다시 말해 상위 스코프에 접근할 수 있는 것은 렉시컬 스코프의 레퍼런스를 차례대로 저장하고 있는 실행 컨텍스트의 스코프 체인을 자바스크립트 엔진이 검색하였기에 가능한 것이다. 좀더 자세히 설명하면 아래와 같다.

  1. innerFunc 함수 스코프(함수 자신의 스코프를 가리키는 활성 객체) 내에서 변수 x를 검색한다. 검색이 실패하였다.

  2. innerFunc 함수를 포함하는 외부 함수 outerFunc의 스코프(함수 outerFunc의 스코프를 가리키는 함수 outerFunc의 활성 객체)에서 변수 x를 검색한다. 검색이 성공하였다.

클로저(Closure)

이번에는 내부 함수 innerFunc 을 함수 outerFunc 내에서 호출하는 것이 아니라 반환하도록 변경해 보자.

function outerFunc() {
  var x = 10;
  var innerFunc = function () { console.log(x); };
  return innerFunc;
}

var inner = outerFunc();
inner(); // 10

함수 outerFunc은 내부함수 innerFunc을 반환하고 생을 마감한다. 즉, 함수 outerFunc은 실행된 이후 콜스택에서 제거되었으므로 함수 outerFunc의 변수 x 또한 더 이상 유효하지 않게 되어 변수 x에 접근할 수 있는 방법은 없어 보인다. 그러나 위 코드의 실행 결과는 변수 x의 값인 10이다. 이미 life-cycle이 종료되어 실행 컨텍스트 스택에서 제거된 함수 outerFunc의 지역변수 x가 다시 부활한 듯이 동작하고 있다.

이처럼 자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는데 이러한 함수를 클로저(Closure)라고 부른다.

클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다.

위 정의에서 말하는 "함수"란 반환된 내부함수를 의미하고 "그 함수가 선언될 때의 렉시컬 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수를 말한다.

이를 조금 더 간단히 말하자면 클로저는 자신이 생성될 때의 환경(Lexical environment)을 기억하는 함수다.

클로저의 활용

클로저는 자신이 생성될 때의 환경(Lexical environment)을 기억해야 하므로 메모리 차원에서 손해를 볼 수 있다. 하지만 클로저는 자바스크립트의 강력한 기능으로 이를 적극적으로 활용해야 한다.

  • 데이터 은닉: 클로저를 사용하면 함수 내에서 선언한 변수가 해당 함수 스코프 내에서만 접근 가능하게 된다. 이를 통해 변수의 값과 상태를 보호하고 외부에서 직접 접근하지 못하게 할 수 있다.

  • 지역 변수 보존: 클로저 내부에서 선언된 변수는 클로저 외부에서 호출되어도 해당 변수의 값이 유지된다. 이는 함수가 호출되는 시점에서의 상태를 보존할 수 있다는 의미이며, 비동기 처리나 콜백 함수등에서 유용하게 사용된다.

  • 콜백 함수와 비동기 처리: 비동기 작업에서 클로저를 사용하면 해당 작업이 완료될 때까지 데이터를 유지하고 사용할 수 있다. 이를 통해 콜백 지옥과 같은 코드 복잡성을 줄이고 데이터를 쉽게 전달할 수 있다.

  • 함수 팩토리: 클로저를 활용하여 함수를 동적으로 생성할 수 있다. 함수 팩토리 패턴은 함수를 호출할 때마다 다른 지역 변수를 가지는 함수를 생성할 수 있는데, 이는 상태 유지와 모듈 패턴 등에 활용된다.

  • 메모리 관리와 성능 최적화: 클로저를 사용할 때 필요한 변수들만 클로저 내부에 포함시키면 불필요한 메모리 사용을 줄일 수 있다. 또한 클로저를 사용하면 전역 변수를 줄여 코드 충돌과 메모리 누수를 방지할 수 있다.

상태 유지

크로저가 가장 유용하게 사용되는 상황은 현재 상태를 기억하고 변경된 최신 상태를 유지하는 것이다.

<!DOCTYPE html>
<html>
<body>
  <button class="toggle">toggle</button>
  <div class="box" style="width: 100px; height: 100px; background: red;"></div>

  <script>
    var box = document.querySelector('.box');
    var toggleBtn = document.querySelector('.toggle');

    var toggle = (function () {
      var isShow = false;

      // ① 클로저를 반환
      return function () {
        box.style.display = isShow ? 'block' : 'none';
        // ③ 상태 변경
        isShow = !isShow;
      };
    })();

    // ② 이벤트 프로퍼티에 클로저를 할당
    toggleBtn.onclick = toggle;
  </script>
</body>
</html>

① 즉시실행함수는 함수를 반환하고 즉시 소멸한다. 즉시실행함수가 반환한 함수는 자신이 생성됐을 때의 렉시컬 환경(Lexical environment)에 속한 변수 isShow를 기억하는 클로저다. 클로저가 기억하는 변수 isShow는 box 요소의 표시 상태를 나타낸다.

② 클로저를 이벤트 핸들러로서 이벤트 프로퍼티에 할당했다. 이벤트 프로퍼티에서 이벤트 핸들러인 클로저를 제거하지 않는 한 클로저가 기억하는 렉시컬 환경의 변수 isShow는 소멸하지 않는다. 다시 말해 현재 상태를 기억한다.

③ 버튼을 클릭하면 이벤트 프로퍼티에 할당한 이벤트 핸들러인 클로저가 호출된다. 이때 .box 요소의 표시 상태를 나타내는 변수 isShow의 값이 변경된다. 변수 isShow는 클로저에 의해 참조되고 있기 때문에 유효하며 자신의 변경된 최신 상태를 게속해서 유지한다.

이처럼 클로저는 현재 상태를 기억하고 이 상태가 변경되어도 최신 상태를 유지해야 하는 상황에 매우 유용하다. 만약 자바스크립트에 클로저라는 기능이 없다면 상태 유지를 위해 전역 변수를 사용할 수 밖에 없다. 전역 변수는 언제든지 누구나 접근할 수 있고 변경할 수 있기 때문에 많은 부작용을 유발해 오류의 원인이 되므로 사용을 억제해야 한다.

데이터 은닉

이번에는 생성자 함수 Counter를 생성하고 이를 통해 counter 객체를 만들어보자.

function Counter() {
  // 카운트를 유지하기 위한 자유 변수
  var counter = 0;

  // 클로저
  this.increase = function () {
    return ++counter;
  };

  // 클로저
  this.decrease = function () {
    return --counter;
  };
}

const counter = new Counter();

console.log(counter.increase()); // 1
console.log(counter.decrease()); // 0

생성자 함수 Counter는 increase, decrease 메소드를 갖는 인스턴스를 생성한다. 이 메소드들은 모두 자신이 생성됐을 때의 렉시컬 환경인 생성자 함수 Counter의 스코프에 속한 변수 counter를 기억하는 클로저이며 렉시컬 환경을 공유한다. 생성자 함수가 함수가 생성한 객체의 메소드는 객체의 프로퍼티에만 접근할 수 있는 것이 아니며 자신이 기억하는 렉시컬 환경의 변수에도 접근할 수 있다.

이때 생성자 함수 Counter의 변수 counter는 this에 바인딩된 프로퍼티가 아니라 변수다. counter가 this에 바인딩된 프로퍼티라면 생성자 함수 Counter가 생성한 인스턴스를 통해 외부에서 접근이 가능한 public 프로퍼티가 되지만 생성자 함수 Counter 내에서 선언된 변수 counter는 생성자 함수 Counter 외부에서 접근할 수 없다. 하지만 생성자 함수 Counter가 생성한 인스턴스의 메소드인 increase, decrease는 클로저이기 때문에 자신이 생성됐을 때의 렉시컬 환경인 생성자 함수 Counter의 변수 counter에 접근할 수 있다. 이러한 클로저의 특징을 사용해 클래스 기반 언어의 private 키워드를 흉내낼 수 있다.

참고
[MDN] 클로저
[모던 자바스크립트 딥다이브] 클로저

profile
꾸준하게

0개의 댓글