🎵 오늘의 음악 🎵
Closure와 아무 상관없는 '오마이걸의 Closer' 추천~!
✏️ 오늘의 끄적끄적 ✏️
Closure를 알려면 렉시컬 Scope를 알아야 하고,
렉시컬 Scope를 알려면 Scope를 알아야 하고,
이 모든 것을 이해하려면, 실행 컨텍스트와 스코프 체인까지 알아야 한다.
클로저는 함수를 지칭하고 또 그 함수가 선언됐을 때의 어휘적 환경(렉시컬 환경)과의 조합
→ 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다.
→ 클로저는 자신이 생성될 당시의 환경을 기억하고, 함수가 호출되는 환경과 별개로 기존에 선언되어 있던 환경을 기준으로 변수를 조회한다.
=> 즉, 내부함수가 자신이 선언됐을 때의 환경인 렉시컬 스코프(Lexical Scope)를 기억하여, 렉시컬 스코프 밖에서 호출하거나 실행될 때도 그 환경에 접근할 수 있게 해준다.
클로저의 핵심은 스코프를 이용해서, 변수의 접근 범위를 닫는 것
→ 내부함수에서는 외부함수 스코프에서 선언된 지역변수에 접근이 가능하다.
→ 외부함수의 실행이 끝나서 소멸된 이후에도 내부함수가 외부함수 내 변수를 사용할 수 있다.
→ 특정 데이터를 스코프 안에 가두어 둔 채로 계속 사용할 수 있게하는 폐쇄성을 가진다.
=> 클로저에 참조되는 외부함수의 지역변수를 자유변수(Free variable)라고 부른다.
function outer() {
const name = 'Bin';
function inner() {
console.log(name); // Bin
}
inner();
}
outer(); // Bin
- 변수
name
은 외부함수에 정의되어 있는 지역변수(자유변수)다.
- 내부함수
inner()
가 호출되었을 때,inner()
내부에서 변수 검색을 실패하니까 외부함수outer()
에 접근해 변수를 찾게 되고 검색에 성공한다.
why?
- 내부에 선언된 내부 함수니까, 자신이 속한 렉시컬 스코프를 참조할 수 있다!
- 내부함수inner()
의 상위 스코프는 외부함수outer()
이다.
- 따라서 함수outer()
를 호출하면 'bin' 을 잘 출력한다.=> 내부함수에서 외부함수의 변수에 접근하는 것이 바로 클로저의 핵심이다.
function outer() {
const x = 'Hello';
const y = 'Bin!';
const inner = function () {
console.log(x + ' ' + y);
};
inner();
}
outer(); // Hello Bin!
- 외부함수
outer()
내에 내부함수inner
가 선언되고 호출된다.
- 내부함수
inner
내에서 변수x
와y
를 찾지만 실패한다.
- 내부함수
inner
는 자신을 포함하고 있는 외부함수outer()
의 변수x
와y
에 접근해서 변수 찾기에 성공한다.
< 실행 컨텍스트 관점에서 설명 >
그 전에 실행 컨텍스트와 스코프 체인을 먼저 알아야 한다. 클릭!
- 위 예제 01, 02는 내부함수를 외부함수 내에서 호출할 때이다.
- 내부함수가 호출되면 자신의 실행 컨텍스트가 스택에 쌓이고 변수 객체 , 스코프 체인 , this에 바인딩할 객체가 결정된다.
- 이때 스코프 체인은 전역 스코프를 가리키는 전역 객체와 외부함수의 활성 객체, 그리고 함수 자신의 스코프를 가리키는 활성 객체를 순차적으로 바인딩한다.
=> 스코프 체인이 바인딩한 객체가 바로 렉시컬 스코프의 실체다.
다시 말하자면 :
내부함수가 외부함수의 변수에 접근할 수 있는 것 즉, 상위 스코프에 접근할 수 있는 것은 렉시컬 스코프의 레퍼런스를 차례대로 저장하고 있는 실행 컨텍스트의 스코프 체인을 자바스크립트 엔진이 검색하였기에 가능한 것이다.
function outer() {
const x = 'Hello';
const y = 'Bin!';
const inner = function () {
console.log(x+ ' ' + y);
};
return inner;
}
const myFunc = outer();
myFunc(); // Hello Bin!
- 함수
outer()
를 호출하면 내부함수inner
를 반환하고 종료한다.
- 함수
myFunc
은 함수outer()
의 리턴값이다.
- 함수
outer()
는 실행된 이후 콜스택(실행 컨텍스트 스택)에서 제거되었기에 함수outer()
의 변수x
,y
또한 더이상 유효하지 않게 되어 변수에 접근할 방법이 없어 보인다.
but!
- 위 코드의 실행 결과는 변수
x
,y
의 값인 'Hello Bin!' 이 출력된다.
- 이미 Life-Cycle ( 생성되고 사용되며 소멸되는 메커니즘 )이 종료되어 실행 컨텍스트 스택에서 제거된 함수outer()
의 지역변수가 다시 동작하고 있다.
why?
- 클로저라면 가능하다.
- 자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부함수가 호출되더라도 외부함수의 지역변수에 접근할 수 있기 때문이다.=> 외부함수가 종료된 이후에도 내부함수를 통해 변수에 접근 가능한 것이 클로저 특성이다.
< 실행 컨텍스트 관점에서 설명 >
내부함수가 유효한 상태에서 외부함수가 종료하여 외부함수의 실행 컨텍스트가 반환되어도, 외부함수 실행 컨텍스트 내의 활성 객체 ( Activation object (AO) - 변수, 함수 선언 등의 정보를 가지고 있음 )는 내부함수에 의해 참조되는 한 유효하여 내부함수가 스코프 체인을 통해 참조할 수 있는 것을 의미한다.
아래 그림을 보면서 이해해 보자 ↓
클로저를 가장 유용하게 사용하는 상황은 현재 상태를 기억하고 변경된 최신 상태를 유지하는 것
아래의 부분 예제는 토글버튼을 통해 박스가 보였다 안보였다 하는 결과를 출력한다.
<body>
<div class="container">
<button class="toggle">toggle</button>
<div class="box"></div>
</div>
<script>
let box = document.querySelector('.box');
let toggleBtn = document.querySelector('.toggle');
let toggle = (function () {
let showPurpleBox = false;
// 1) 클로저를 반환
return function () {
box.style.display = showPurpleBox ? 'block' : 'none';
// 3) 상태 변경
showPurpleBox = !showPurpleBox;
};
})();
// 2) 이벤트 프로퍼티에 클로저를 할당
toggleBtn.onclick = toggle;
</script>
</body>
들어가기에 앞서 즉시실행함수란?말 그대로 즉시 실행되는 함수 즉, 함수를 정의하는 동시에 호출되는 함수다.
익명 함수를 사용하는 것이 일반적이며, 한 번 밖에 실행할 수 없다.
< 코드 설명 1 >
- 일단, 즉시실행함수는 먼저 실행되고 함수를 반환하고 즉시 소멸한다.
- 즉시실행함수가 반환한 함수는 자신이 생성됐을 때의 렉시컬 환경에 속한 변수showPurpleBox
를 기억하는 클로저다.
< 코드 설명 2 >
- 클로저가 기억하는 변수
showPuurpleBox
는 .box 요소의 표시 상태를 나타낸다.
- 그 다음, 이 클로저를 이벤트 핸들러로서 이벤트 프로퍼티에 할당했다.
- 이벤트 프로퍼티에서 이벤트 핸들러인 클로저를 제거하지 않는 이상 클로저가 기억하는 렉시컬 환경의 변수showPurpleBox
는 소멸하지 않는다. ( 즉, 현재 상태를 기억하는 것 )
< 코드 설명 3 >
- 토글버튼을 클릭하면 이벤트 프로퍼티에 할당한 이벤트 핸들러인 클로저가 호출된다.
- 이때 .box 요소의 표시 상태를 나타내는 변수showPurpleBox
의 값이 변경된다.
- 변수showPurpleBox
는 클로저에 의해 참조되고 있기 때문에 유효하며 자신의 변경된 최신 상태를 게속해서 유지한다.
결론을 말하자면 :
클로저는 현재 상태를 기억하고 이 상태가 변경되어도 최신 상태를 유지해야 하는 상황에 매우 유용하다.
참고 :
만약 자바스크립트에 클로저라는 기능이 없다면 상태를 유지하기 위해 전역 변수를 사용할 수 밖에 없다. 전역 변수는 언제든지 누구나 접근할 수 있고 변경할 수 있기 때문에 많은 부작용을 유발해 오류의 원인이 되므로 사용을 억제해야 한다.
전역 변수와 지역 변수를 사용한 Counting
< 전역 변수 >
- 코드는 잘 동작하지만, 오류를 발생시킬 가능성을 내포하고 있다.
-plus
함수는 호출되기 직전에 전역변수counter
의 값이 반드시 0이어야 제대로 동작한다.
- 하지만, 변수counter
는 전역변수이기에 언제든지 누구나 접근할 수 있고 변경할 수 있다.
- 만약 누군가 의도치 않게 전역변수 값을 변경했다면 곧바로 오류로 이어진다.=> 변수
counter
는 카운터를 관리하는plus
함수가 관리하는 것이 바람직하다.
< 지역 변수 >
- 전역변수를 지역변수로 변경해 일단 의도치 않은 상태 변경은 방지했다.
- 하지만,plus
함수가 호출될 때마다 지역변수counter
를 0으로 초기화하기 때문에 언제나 1이 표시된다.
- 즉, 변경되기 이전 상태를 기억하지 못하는 것이다.=> 이전 상태를 기억하도록 클로저를 사용해 문제를 해결해 보자!
<body>
<div class="container">
<p>클로저를 사용한 Counting</p>
<button id="plus">+</button>
<p id="count">0</p>
</div>
<script>
let plusBtn = document.getElementById('plus');
let count = document.getElementById('count');
let plus = (function () {
// 카운트 상태를 유지하기 위한 자유 변수
let counter = 0;
// 클로저를 반환
return function () {
return ++counter;
};
})();
plusBtn.onclick = function () {
count.innerHTML = plus();
};
</script>
</body>
클로저를 사용한 Counting
- 스크립트가 실행되면 즉시실행함수가 호출되고
변수plus
에는 함수function () { return ++conunter; }
가 할당된다.
- 이 함수는 자신이 생성됐을 때의 렉시컬 환경을 기억하는 클로저다.
- 즉시실행함수는 호출된 이후 소멸되지만, 즉시실행함수가 반환한 함수는 변수
plus
에 할당되어plus
버튼을 클릭하면 클릭 이벤트 핸들러 내부에서 호출된다.
- 이때 클로저인 이 함수는 자신이 선언됐을 때의 렉시컬 환경인 즉시실행함수의 스코프에 속한 지역변수 (자유변수)
counter
를 기억한다.
- 따라서 즉시실행함수의 변수
counter
에 접근할 수 있고 변수counter
는 자신을 참조하는 함수가 소멸될 때가지 유지된다.
- 즉시실행함수는 한번만 실행되므로
plus
함수가 호출될 때마다 변수counter
가 재차 초기화될 일은 없을 것이다.
- 변수
counter
는 외부에서 직접 접근할 수 없는 private 변수이므로 전역변수를 사용했을 때와 같이 의도되지 않은 변경을 걱정할 필요도 없기 때문에 보다 안정적인 프로그래밍이 가능하다.
function Counter() {
// 카운트를 유지하기 위한 자유 변수
let 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 키워드를 흉내낼 수 있다.
// 입력
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]());
}
// 출력
5
5
5
5
5
< 코드 설명 >
- 첫번째
for문
의 코드 블록 내에서 함수가arr
배열 요소로 추가된다.
- 두번째
for문
의 코드 블록 내에서arr
배열의 요소로 추가된 함수를 순차적으로 호출한다.
- 이때
arr
배열의 요소로 추가된 5개의 함수가 0, 1, 2, 3, 4 를 반환할 것 같지만, 결과는 그렇지 않다.
for문
에서 사용한 변수i
는 블록 레벨 스코프가 아닌 함수 레벨 스코프를 갖기 때문에 전역변수다. ( 변수 호이스팅 발생 )
- 전역변수
i
에 0, 1, 2, 3, 4 가 순차적으로 할당된다.
- 따라서
arr
배열의 요소로 추가한 함수를 호출하면, 전역변수i
를 참조하여i
의 값 5가 출력된다.
var arr = [];
for (var i = 0; i < 5; i++){
arr[i] = (function (id) { // ②
return function () {
return id; // ③
};
}(i)); // ①
}
for (var j = 0; j < arr.length; j++) {
console.log(arr[j]());
}
< 코드 설명 >
- 배열
arr
에는 즉시실행함수에 의해 함수가 반환된다.
- 이때 즉시실행함수는
i
를 인자로 전달받고 매개변수id
에 할당한 후 내부 함수를 반환하고 종료된다. 매개변수id
는 자유변수가 된다.
( 함수의 매개변수는 자동으로 지역변수 취급 )
- 배열
arr
에 할당된 함수는id
를 반환한다. 이때id
는 상위 스코프의 자유변수이므로 그 값이 유지된다.
=> 위 예제는 자바스크립트의 함수 레벨 스코프 특성으로 인해
for
루프의 초기문에서 사용된 변수의 스코프가 전역이 되기 때문에 발생하는 현상이다.
const arr = [];
for (let i = 0; i < 5; i++) {
arr[i] = function () {
return i;
};
}
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]());
}
< 코드 설명 >
for문
의 변수 선언문에서let
키워드로 선언한 변수를 사용하면for문
의 코드 블록이 반복 실행될 때마다for문
코드 블록의 새로운 렉시컬 환경이 생성된다.
- 만약
for문
의 코드 블록 내에서 정의한 함수가 있다면, 이 함수의 상위 스코프는for문
의 코드 블록이 반복 실행될 때마다 생성된for문
코드 블록의 새로운 렉시컬 환경이다.
- 이때 함수의 상위 스코프는
for문
의 코드 블록이 반복 실행될 때마다 식별자 ( for문의 변수 선언문에서 선언한 초기화 변수 및 for문의 코드 블록 내에서 선언한 지역 변수 ) 값을 유지해야 한다.
- 이를 위해
for문
이 반복될 때마다 독립적인 렉시컬 환경을 생성하여 식별자의 값을 유지한다.
=> 이처럼
let
,const
키워드를 사용하는 반복문은 코드 블록을 반복 실행할 때마다 새로운 렉시컬 환경을 생성하여 반복할 당시의 상태를 마치 스냅샷을 찍는 것처럼 저장한다.단, 이는 반복문의 코드 블록 내부에서 함수를 정의할 때 의미가 있다.
반복문의 코드 블록 내부에 함수 정의가 없는 반복문이 생성하는 새로운 렉시컬 환경은 반복 직후, 아무도 참조하지 않기 때문에 가비지 컬렉션의 대상이 된다.
결론을 말하자면 :클로저는 자신이 생성될 때의 환경을 기억해야하므로 퍼포먼스 저하 및 메모리 적인 측면에서 손해를 보지만... 네버 더 레스! 과연... 자바스크립트에서 클로저를 빼면 자바스크립트라고 할 수 있을까? 따라서! 클로저는 단점보다 장점이 훨씬 많고 강력한 기능이다.