JavaScript 클로저

seul_velog·2022년 1월 3일
1

JavaScript

목록 보기
19/25
post-thumbnail

클로저(Closure)란?

클로저는 내부함수가 외부함수의 맥락(context)에 접근할 수 있는 것을 가르킨다.

  • 자바스크립트 고유의 개념이 아닌, 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 특징이다.
  • MDN에서는 '클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합'이라고 정의한다.





렉시컬 스코프

자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해하자.

정적 유효범위

자바스크립트는 함수가 선언된 시점에서의 유효범위를 갖는다. 이러한 유효범위의 방식을 정적 유효범위(static scoping), 혹은 렉시컬(lexical scoping)이라고 한다.

var i = 5;
 
function a(){
    var i = 10;
    b();
}
 
function b(){
    document.write(i);
}
 
a();

변수 i 는 전역변수이다.
함수 a 는 변수 i 를 지역변수로 가지고 있으며 다시 함수 b 를 호출한다.
함수 b 는 변수 i 를 출력한다.

1) 함수 a 를 호출한다. → 함수 내부 i 값=10 이되며, 그 후 함수 b 를 출력한다.
2 ) i 는 먼저 함수 b 내부에 i 지역변수를 찾는다.
3 ) 지역변수가 없다면 전역 변수를 찾는다.
이때, b 를 호출하고 있는 함수 a 의 지역변수 VS 함수b 가 현재 정의된 시점에서의 전역변수 중 어느것을 따라갈까?

→ 답은 i = 5 이다.
함수 b 가 선언된 시점에서 i 의 전역변수가 사용된다. 함수b 가 호출된 시점은 영향을 주지 못한다.
'사용될 때' 가 아닌 '정의될 때' 사용된다. 이것을 정적 유효범위, 렉시컬 스코핑 이라고 한다.
함수가 호출된 위치는 스코프 결정에 아무런 의미를 주지 않는다.

seul-scope





내부함수와 외부함수의 관계

function outter(){
    function inner(){
        const title = 'something'; 
        alert(title);
    }
    inner();
}
  • 예제의 funtion inner(){...} 를 내부 함수라고 한다.
  • 어떤 함수의 내부에서만 사용되는 함수가 필요할 경우, 그 함수를 바깥쪽에 선언하면 응집성이 떨어지기 때문에, 함수 안에 함수를 선언함으로써 가독성을 돕고 편리하게 사용할 수 있다.




클로저 이해하기

✍️ 예제를 통해 클로저의 특징에 대해서 알아보자.

내부함수에서 외부함수의 지역변수에 접근할 수 있다.

function outter(){
    const title = 'Hello World!';  
    function inner(){        
        alert(title);
    }
    inner();
}
outter();
  • 클로저는 내부함수와 밀접한 관계를 가지고 있다.

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

  • 즉, 내부함수inner 에서 외부함수outter 의 지역변수title 에 접근할 수 있다. 이러한 것을 클로저 라고한다.



외부함수가 실행이 종료되어 소멸된 이후에도, 내부함수가 외부함수의 변수에 접근 할 수 있다.

// ex.1)
function outter(){
    const title = 'Hello World!';  
    return function(){        
        alert(title);
    }
}
inner = outter();
inner();
  • 흔히 함수 내에서 함수를 정의하고 사용하면 클로저라고 한다. 하지만 대개는 정의한 함수를 리턴하고 사용은 바깥에서 하게된다.

  • 위에서 정의한 otter 은 함수를 반환하고, 반환된 함수는 otter 내부에서 선언된 변수 title 을 참조하고 있다.

  • 참조된 변수는 함수 실행이 끝났다고 해서 사라지지 않았고, 여전히 값을 반환하고 있다.

  • 즉, 내부함수가 외부함수의 지역변수에 접근 할 수 있고, 외부함수는 외부함수의 지역변수를 사용하는 내부함수가 소멸될 때까지 소멸되지 않는 클로저의 특성을 볼 수 있다.


// ex.2)
const base = 'hello! ';

function sayHelloTo(name) {
const result = base + name;
return function(){
console.log(result);
}
}

const hello1 = sayHelloTo('kim');
const hello2 = sayHelloTo('lee');

hello1();  // hello! kim
hello2();  // hello! lee
  • 위에서 정의한 sayHelloTo 는 함수를 반환하고, 반환된 함수는 sayHelloTo 내부에서 선언된 변수 result 를 참조하고 있다.
  • 출력을 보면 변수 result 가 동적으로 변화한 것 처럼 보이지만, 실제로는 result 라는 변수 자체가 여러 번 생성된 것이다.
  • hello1() , hello2() 는 서로 다른 환경을 가지고 있다.



클로저는 객체의 메소드에서도 사용할 수 있으며, 클로저를 통해 Private한 속성을 사용할 수 있다.

// ex.1)
function movie(title){
  return {
      get_title : function(){
        return title;
      },
      set_title : function(_title){
        title = _title
      }
    }
  }
  
const myMovie1 = movie('a');
const myMovie2 = movie('b');

console.log(myMovie1.get_title());  // a
console.log(myMovie2.get_title());  // b

myMovie1.set_title('aa');
myMovie2.set_title('bb');

console.log(myMovie1.get_title());  // aa 값이 변경되었다.
console.log(myMovie2.get_title());  // bb 값이 변경되었다.
  • 클로저는 객체의 메소드에서도 사용할 수 있다.
    메소드 get_titleset_title 은 외부 함수 movie 의 인자값으로 전달된 지역변수title 을 사용하고 있다.

  • 동일한 외부함수 안에서 만들어진 내부함수나 메소드는 외부함수의 지역변수를 공유한다.
    1) myMovie1myMovie2set_tile 에서 외부함수의 지역변수 title 을 변경했다.
    2) 이후 get_title 의 값이 변경 되었다.

  • JavaScript는 기본적으로 Private한 속성을 지원하지 않지만, 클로저의 특성을 이용해서 Private한 속성을 사용할 수 있다.
    1) movie 의 지역변수 title 은 2행에서 정의된 객체의 메소드에서만 접근 할 수 있는 값이다.
    2) 이 말은 title 의 값을 읽고 수정 할 수 있는 것은 movie 메소드를 통해서 만들어진 객체 뿐이라는 의미다.


// ex.2) 클로저를 통한 은닉화 추가예제
function music(title) {
const _title = title;
return function(){
console.log('My favorite music is ' + _title);
}
}

const music1 = music('a');
const music2 = music('b');

music1();  // My favorite music is a
music2();  // My favorite music is b
  • 특별히 인터페이스를 제공하는 것이 아니라면, 여기서는 외부에서 _title 에 접근할 방법이 없다.



클로저와 반복문

 var arr = []
   for(var i = 0; i < 3; i++){
     arr[i] = function(){
       return i; // i를 리턴한다.
   }
 }
 
 for(var index in arr){
 	console.log(arr[index]()); // return i의 값으로 치환된다.
 }

// 3
// 3
// 3
  • 클로저를 사용할 때 자주 발생할 수 있는 실수에 관련한 예제이다. 예상한 0, 1, 2 출력이 되지 않는다.

  • 이유는, 3행 에 정의된 함수의 외부 변수에 정의된 i 의 값이 아니므로, 즉 for문에서 사용한 변수 i 는 전역 변수이기 때문이다.

  • ❓ 그렇다면, 어떻게 만들어야 할까?
    1)3행 에 정의된 함수를 내부 함수로 하는 새로운 외부 함수를 정의한다.
    2) 그 외부함수에 지역변수의 값을 준다.
    3) 내부함수가 외부함수의 지역변수를 참조할 수 있도록 만든다.


▼ 수정된 예제를 참고하자.

var arr = []
for(var i = 0; i < 3; i++){
    arr[i] = function(a) {  // 2) 
        return function(){
            return a;  // 3) 
        }
    }(i);  // 1) 외부함수를 즉석으로 호출 -> 외부함수를 리턴 -> 내부함수를 리턴
}
for(var index in arr) {
    console.log(arr[index]());
}

// 0
// 1
// 2

이전 3행 의 함수를 내부 함수로 하는 새로운 외부 함수를 정의한다.
1) 즉시실행 함수를 통해 i 값을 인자로 넣어준다.
2) a 에 인자값 i 가 들어온다. (a 는 일종의 지역변수와 같은 효과를 낸다.)
3) 들어온 인자값을 함수내의 지역변수처럼 사용할 수 있게 내부 함수를 만들어 줌으로써, 리턴 값으로 들어온 인자값을 가리킨다.
4) 리턴된 내부 함수는 arr[i] 에 담기게 된다.


📌 위 예제는 자바스크립트의 함수 레벨 스코프 특성으로 인해 for 루프의 초기문에서 사용된 변수의 스코프가 전역이 되기 때문에 발생하는 현상이다.

ES6의 let 키워드를 사용하면 위와 같은 문제는 나타나지 않는다. 😀

const arr = [];

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

for(var index in arr) {
    console.log(arr[index]());
}
// 0
// 1
// 2

✍️ 앞으로 var 대신 let 을 많이 사용한다고 하더라도 기존의 var 와 클로저에 대한 지식이 있어야, 앞으로의 문제 상황에서 유연하게 대처할 수 있을 것 같다고 생각했다.





클로저의 활용

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

정보의 은닉

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

▶ 앞에서 살펴본 바와 같이 클로저의 특성을 이용해서 Private한 속성을 사용할 수 있다.

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

  • 2) 생성자 함수 Counter 의 변수 counterthis 에 바인딩된 프로퍼티가 아니라 변수이다.
    → 생성자 함수 Counter 내에서 선언된 변수 counter 는 생성자 함수 Counter 외부에서 접근할 수 없다.

  • 3) 생성자 함수 Counter 가 생성한 인스턴스의 메소드인 increase , decrease 는 클로저이기 때문에 자신이 생성됐을 때의 렉시컬 환경인 생성자 함수 Counter 의 변수 counter 에 접근할 수 있다.

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



상태유지

<!-- html ▼ -->
 <button class="toggle">toggle</button>
  <div class="box" style="width: 80px; height: 80px; background: blue;"></div>
// javaScript ▼
 var box = document.querySelector('.box');
 var toggleBtn = document.querySelector('.toggle');

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

    // 2) 이벤트 프로퍼티에 클로저를 할당
    toggleBtn.onclick = toggle;

▶ toggle 버튼을 누르면 나타나거나 사라지는 box를 만든다.

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

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

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

📌 클로저가 가장 유용하게 사용되는 상황은 현재 상태를 기억하고 변경된 최신 상태를 유지하는 것이다.
예제의 클로저 기능을 사용하지 않았더라면 전역변수를 사용해 할 것이다.



전역 변수의 사용 억제

<!-- html ▼ -->
<button id="inclease">+</button>
<p id="count">0</p>
// javaScript ▼
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();
};

▶ 버튼이 클릭될 때마다 클릭한 횟수가 누적되어 화면에 표시되는 카운터가 만들어진다. 클릭된 횟수가 유지해야할 상태이다.

  • 1) 즉시실행함수가 호출된다.
    → 변수increase 에는 함수 function(){return ++counter;} 가 할당된다. 이 함수는 자신이 생성됐을 때의 렉시컬 환경을 기억하는 클로저이다.

  • 2) 즉시실행함수는 호출된 이후 소멸한지만 반환한 함수는 변수 increase 에 할당되어, inclease 버튼을 클릭하면 이벤트 핸들러 내부에서 호출된다.
    → 이때 클로저인 이 함수는 자신이 선언됐을 때 렉시컬환경인 즉시실행함수의 스코프에 속한 지역변수 counter 를 기억한다.

  • 3) 따라서 즉시실행함수의 변수 counter 에 접근할 수 있다.
    → 변수 counter 는 자신을 참조하는 함수가 소멸될 때까지 유지된다.

  • 4) 즉시실행함수는 한번만 실행된다.
    → increase가 호출될 때마다 변수 counter가 재차 초기화될 일은 없다.

  • 5) 변수 counterprivate 변수이다.
    → 전역변수보다 안정적인 프로그래밍이 가능하다.

📌 변수의 값은 누군가에 의해 언제든지 변경될 수 있어 오류 발생의 원인이 될 수 있다. 상태 변경이나 가변(mutable) 데이터를 피하고 불변성(Immutability)을 지향하는 함수형 프로그래밍에서 이러한 오류를 방지하고, 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용된다고 한다. 🤔






reference
coding everybody
hyunseob - closure
poiemaweb - closure
MDN-Closure
meetup
joheresig

profile
기억보단 기록을 ✨

0개의 댓글