JavaScript

18. Closure 클로저

js0616 2024. 7. 19. 11:17

https://poiemaweb.com/js-closure

 

Closure | PoiemaWeb

클로저(closure)는 자바스크립트에서 중요한 개념 중 하나로 자바스크립트에 관심을 가지고 있다면 한번쯤은 들어보았을 내용이다. execution context에 대한 사전 지식이 있으면 이해하기 어렵지 않

poiemaweb.com

 

클로저는 자바스크립트 고유의 개념이 아니라 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다.

 

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

 


 

1. 실행 컨텍스트 (Execution Context)
실행 컨텍스트는 코드가 실행되는 환경을 의미합니다. 자바스크립트에서 코드가 실행될 때마다 실행 컨텍스트가 생성되며, 이는 실행 중인 코드의 상태를 기록하고 관리합니다. 각 실행 컨텍스트는 변수 객체(Variable Object), 스코프 체인(Scope Chain), this 바인딩 등의 정보를 포함합니다.

2. 렉시컬 스코프 (Lexical Scope)
렉시컬 스코프는 함수가 선언될 때 정적으로 결정되는 스코프입니다. 함수가 어디에서 선언되었는지에 따라 함수의 스코프가 결정됩니다. 이것은 함수가 호출될 때가 아니라 선언될 때 이미 결정되는 것을 의미합니다.

 

 

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

outerFunc(); // 10

 

실행 과정 설명:

  1. outerFunc() 호출: outerFunc 함수가 호출되면 새로운 실행 컨텍스트가 생성됩니다.
  2. outerFunc의 실행 컨텍스트:
    • outerFunc의 변수 객체(Activation Object)가 생성됩니다. 여기에는 x 변수가 포함됩니다.
    • outerFunc의 스코프 체인은 전역 스코프를 가리키는 전역 객체와 outerFunc의 활성 객체를 가리킵니다.
  3. innerFunc 선언: outerFunc 내부에서 innerFunc 함수가 선언됩니다.
    • innerFunc 함수는 outerFunc의 변수 x를 참조합니다. 이때 innerFunc의 렉시컬 스코프 체인은 자신의 활성 객체와 outerFunc의 활성 객체를 포함하게 됩니다.
  4. innerFunc 호출: innerFunc 함수가 호출되면 새로운 실행 컨텍스트가 생성됩니다.
    • innerFunc의 변수 객체는 생성됩니다. 이때는 x 변수가 없지만, 스코프 체인에서 참조할 수 있습니다.
    • innerFunc의 스코프 체인은 자신의 변수 객체와 outerFunc의 변수 객체를 차례로 검색할 수 있는 구조가 됩니다.
  5. 변수 검색: innerFunc에서 x 변수를 출력하려고 시도할 때,
    • innerFunc의 변수 객체에는 x 변수가 없기 때문에, 바로 바깥의 스코프 체인에서 검색을 시작합니다.
    • outerFunc의 변수 객체에서 x 변수를 찾아서 값을 출력하게 됩니다.

 

innerFunc 함수가 outerFunc의 변수 x에 접근할 수 있는 이유는 렉시컬 스코프 체인이 outerFunc의 실행 컨텍스트에서 innerFunc의 실행 컨텍스트로 이어지기 때문입니다.

 


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

/**
 *  함수 outerFunc를 호출하면 내부 함수 innerFunc가 반환된다.
 *  그리고 함수 outerFunc의 실행 컨텍스트는 소멸한다.
 */
var inner = outerFunc();
inner(); // 10

 

 

클로저의 작동 원리

  1. 외부 함수의 실행: outerFunc가 호출되어 실행되면서 변수 x가 생성됩니다.
  2. 내부 함수 정의 및 반환: innerFunc 함수가 정의되고, 이 함수가 outerFunc에서 반환됩니다. 이때 innerFunc은 x 변수를 사용하고 있습니다.
  3. 외부 함수의 종료: outerFunc의 실행이 종료되면서 outerFunc의 변수 객체(Activation Object)는 일반적인 실행 컨텍스트 규칙에 따라 메모리에서 해제됩니다.
  4. 클로저의 유지: 그러나 innerFunc 함수가 외부에 반환되어 다른 곳에서 저장되었습니다. 이 때문에 innerFunc은 여전히 자신이 선언될 때의 스코프 체인을 기억하고 있습니다.
  5. 내부 함수 호출: inner()를 호출할 때, innerFunc는 자신이 선언될 때의 스코프 체인을 따라 x 변수를 찾아 그 값을 출력합니다. 이때 x 변수는 이미 외부 함수가 종료된 후에도 유효합니다.

 

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

 

클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수를 의미한다.

 

클로저에 의해 참조되는 외부함수의 변수 즉 outerFunc 함수의 변수 x를 자유변수(Free variable)라고 부른다.

 

외부함수가 이미 반환되었어도 외부함수 내의 변수는 이를 필요로 하는 내부함수가 하나 이상 존재하는 경우 계속 유지된다. 이때 내부함수가 외부함수에 있는 변수의 복사본이 아니라 실제 변수에 접근한다는 것에 주의하여야 한다.

 

 

vo, Variable Object : 변수 객체

sc, scope chain : 스코프 체인

AO, activation object : 활성 객체

 


 

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

2.1 상태 유지
클로저가 가장 유용하게 사용되는 상황은 현재 상태를 기억하고 변경된 최신 상태를 유지하는 것이다. 아래 예제를 살펴보자

 

<!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는 클로저에 의해 참조되고 있기 때문에 유효하며 자신의 변경된 최신 상태를 계속해서 유지한다.

 

클로저는 현재 상태(위 예제의 경우 .box 요소의 표시 상태를 나타내는 isShow 변수)를 기억하고 이 상태가 변경되어도 최신 상태를 유지해야 하는 상황에 매우 유용하다. 

 


 

2.2 전역 변수의 사용 억제
버튼이 클릭될 때마다 클릭한 횟수가 누적되어 화면에 표시되는 카운터를 만들어보자. 이 예제의 클릭된 횟수가 바로 유지해야할 상태이다.

 

<!DOCTYPE html>
<html>
<body>
  <p>전역 변수를 사용한 Counting</p>
  <button id="inclease">+</button>
  <p id="count">0</p>
  <script>
    var incleaseBtn = document.getElementById('inclease');
    var count = document.getElementById('count');

    // 카운트 상태를 유지하기 위한 전역 변수
    var counter = 0;

    function increase() {
      return ++counter;
    }

    incleaseBtn.onclick = function () {
      count.innerHTML = increase();
    };
  </script>
</body>
</html>

 

위 코드는 잘 동작하지만 오류를 발생시킬 가능성을 내포하고 있는 좋지 않은 코드다.

 

변수 counter는 전역 변수이기 때문에 언제든지 누구나 접근할 수 있고 변경할 수 있다.

이는 의도치 않게 값이 변경될 수 있다는 것을 의미한다.

만약 누군가에 의해 의도치 않게 전역 변수 counter의 값이 변경됐다면 이는 오류로 이어진다. 

 

변수 counter는 카운터를 관리하는 increase 함수가 관리하는 것이 바람직하다.

 

<!DOCTYPE html>
<html>
<body>
  <p>지역 변수를 사용한 Counting</p>
  <button id="inclease">+</button>
  <p id="count">0</p>
  <script>
    var incleaseBtn = document.getElementById('inclease');
    var count = document.getElementById('count');

    function increase() {
      // 카운트 상태를 유지하기 위한 지역 변수
      var counter = 0;
      return ++counter;
    }

    incleaseBtn.onclick = function () {
      count.innerHTML = increase();
    };
  </script>
</body>
</html>

 

전역변수를 지역변수로 변경하여 의도치 않은 상태 변경은 방지했다.

하지만 increase 함수가 호출될 때마다 지역변수 counter를 0으로 초기화하기 때문에 언제나 1이 표시된다.

다시 말해 변경된 이전 상태를 기억하지 못한다. 

 

이전 상태를 기억하도록 클로저를 사용하여 이 문제를 해결해보자.

 

<!DOCTYPE html>
<html>
  <body>
  <p>클로저를 사용한 Counting</p>
  <button id="inclease">+</button>
  <p id="count">0</p>
  <script>
    var incleaseBtn = document.getElementById('inclease');
    var count = document.getElementById('count');

    var increase = (function () {
      // 카운트 상태를 유지하기 위한 자유 변수
      var counter = 0;
      // 클로저를 반환
      return function () {
        return ++counter;
      };
    }());

    incleaseBtn.onclick = function () {
      count.innerHTML = increase();
    };
  </script>
</body>
</html>

 

 

스크립트가 실행되면 즉시실행함수(immediately-invoked function expression)가 호출되고

변수 increase에는 함수 function () { return ++counter; }가 할당된다.

이 함수는 자신이 생성됐을 때의 렉시컬 환경(Lexical environment)을 기억하는 클로저다. 

 

즉시실행함수는 호출된 이후 소멸되지만

반환한 함수는 변수 increase에 할당되어 inclease 버튼을 클릭하면 클릭 이벤트 핸들러 내부에서 호출된다.

 

이때 클로저인 이 함수는 자신이 선언됐을 때의 렉시컬 환경인 즉시실행함수의 스코프에 속한 지역변수 counter를 기억한다. 따라서 즉시실행함수의 변수 counter에 접근할 수 있고 변수 counter는 자신을 참조하는 함수가 소멸될 때가지 유지된다.

 

즉시실행함수는 한번만 실행되므로 increase가 호출될 때마다 변수 counter가 재차 초기화될 일은 없을 것이다.

변수 counter는 외부에서 직접 접근할 수 없는 private 변수이므로 전역 변수를 사용했을 때와 같이 의도되지 않은 변경을 걱정할 필요도 없기 때문이 보다 안정적인 프로그래밍이 가능하다.

 


 

// 함수를 인자로 전달받고 함수를 반환하는 고차 함수
// 이 함수가 반환하는 함수는 클로저로서 카운트 상태를 유지하기 위한 자유 변수 counter을 기억한다.
function makeCounter(predicate) {
  // 카운트 상태를 유지하기 위한 자유 변수
  var counter = 0;
  // 클로저를 반환
  return function () {
    counter = predicate(counter);
    return counter;
  };
}

// 보조 함수
function increase(n) {
  return ++n;
}

// 보조 함수
function decrease(n) {
  return --n;
}

// 함수로 함수를 생성한다.
// makeCounter 함수는 보조 함수를 인자로 전달받아 함수를 반환한다
const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

// increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동하지 않는다.
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2

console.log(increaser()) // 3

 

predicate 자리에 increase / decrease 를 넣어서 counter 의 값을 변경하는 코드이다.

 

별개의 독립된 렉시컬 환경을 가지기 때문에 counter 의 값이 연동하지 않는다. 

-> increase / decrease 를 만들때, makeCounter 를 호출하면 그때 각각의 렉시컬 환경을 가지게된다

 

따라서 연동하여 증감이 가능한 카운터를 만들려면 렉시컬 환경을 공유하는 클로저를 만들어야 한다.

 


2.3 정보의 은닉
이번에는 생성자 함수 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의 변수 counter는 this에 바인딩된 프로퍼티가 아니라 변수다. 

변수 counter는 생성자 함수 Counter 외부에서 접근할 수 없다.

 

생성자 함수 Counter는 increase, decrease 메소드를 갖는 인스턴스를 생성한다. 

increase, decrease는 클로저이기 때문에 생성됐을 때의 렉시컬 환경인 생성자 함수 Counter의 변수 counter에 접근할 수 있다.

 

이러한 클로저의 특징을 사용해 클래스 기반 언어의 private 키워드를 흉내낼 수 있다.


2.4 자주 발생하는 실수
아래의 예제는 클로저를 사용할 때 자주 발생할 수 있는 실수에 관련한 예제다.

 

var arr = [];

for (var i = 0; i < 5; i++) {
  arr[i] = function () {
    return i;
  };
}

for (var j = 0; j < arr.length; j++) {
  console.log(arr[j]());
}

 

 

배열 arr에 5개의 함수가 할당되고 각각의 함수는 순차적으로 0, 1, 2, 3, 4를 반환할 것으로 기대하겠지만 결과는 그렇지않다. for 문에서 사용한 변수 i는 전역 변수이기 때문이다.

 

var arr = [];

for (let i = 0; i < 5; i++){
  arr[i] = (function (id) { // ②
    return function () {
      return id; // ③
    };
  }(i)); // ①
}

for (let j = 0; j < arr.length; j++) {
  console.log(arr[j]());
}

// console.log(i) // var i = 0 일때 전역변수 i 가 남게 되므로 let 을 사용하자

 

① 배열 arr에는 즉시실행함수에 의해 함수가 반환된다.

 
arr[i] = function (){
  return id;
}
 


② 이때 즉시실행함수는 i를 인자로 전달받고 매개변수 id에 할당한 후 내부 함수를 반환하고 life-cycle이 종료된다. 매개변수 id는 자유변수가 된다.

 
( function (id) { ~~ }(i) )
// () 를 붙여 함수를 실행하며, 바깥 () 를 통해 즉시 실행
// 이때 i 를 인자로 받아 매개변수 id 에 할당
 


③ 배열 arr에 할당된 함수는 id를 반환한다. 이때 id는 상위 스코프의 자유변수이므로 그 값이 유지된다.

 

 

또는 함수형 프로그래밍 기법인 고차 함수를 사용하는 방법이 있다. 이 방법은 변수와 반복문의 사용을 억제할 수 있기 때문에 에플리케이션의 오류를 줄이고 가독성을 좋게 만든다.

 

const arr = new Array(5).fill();

arr.forEach((v, i, array) => array[i] = () => i);

arr.forEach(f => console.log(f()));

 


 

클로저란?

클로저는 반환(return) 되는 내부 함수를 이용하여 외부 함수에 있는 자유변수를 기억하고 값을 변경 할 수 있다. 


클로저(Closure)는 일급 객체 함수(first-class functions)의 개념을 이용하여 스코프(scope)에 묶인 변수를 바인딩 하기 위한 일종의 기술이다. 기능상으로, 클로저는 함수를 저장한 레코드(record)이며, 스코프(scope)의 인수(Factor)들은 클로저가 만들어질 때 정의(define)되며, 스코프 내의 영역이 소멸(remove)되었어도 그에 대한 접근(access)은 독립된 복사본인 클로저를 통해 이루어질 수 있다.

 

https://ko.wikipedia.org/wiki/%ED%81%B4%EB%A1%9C%EC%A0%80_(%EC%BB%B4%ED%93%A8%ED%84%B0_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D)

 

클로저 (컴퓨터 프로그래밍) - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 컴퓨터 언어에서 클로저(Closure)는 일급 객체 함수(first-class functions)의 개념을 이용하여 스코프(scope)에 묶인 변수를 바인딩 하기 위한 일종의 기술이다. 기능상

ko.wikipedia.org

 

 

 

 

 

https://chatgpt.com/