자바스크립트의 기본부터 천천히 공부하고 있다.
공부한 내용을 나만의 방식대로 정리해두어야 나중에 까먹어도 쉽게 다시 이해할 수 있을 것 같아 정리했다...
로 시작했지만, 사실 이번 글은 나 혼자 클로저를 이렇게 저렇게 따라해보며 이해해가는 글에 가깝다.
어떤 개념을 배울때 나에게 가장 중요한건 나의 언어로 정리하는게 가장 중요하다.
내가 클로져를 한 마디로 정의하자면 '생성시점을 기억하는 폐쇄공간'으로 정의 할 것 같다.
클로져를 공부하다보면 스코프(Scope), 스코프 체인(Scope Chain), 실행컨텍스트(Execution Context), 어휘적 환경(Lexical Environment) 등 부수적으로 공부해야할 말들이 굉장히 많이 나온다.
때문에 클로져에 대한 정리를 하려다가 부수적인 공부도 많이 하게 된 것 같다.
근데 그래서...클로져라는 하나의 주제만 가지고 글로 정리를 하기가 애매하다.
그래서 이리저리 따라해가는 과정을 정리해두고, 나중에 기회가 되면 한꺼번에 정리를 하는게 좋을 것 같다.
선결론
클로져는 내부함수가 외부함수의 변수를 참조하며, 함수가 생성됐을 시점의 환경을 기억하는 함수이다.
왜 쓰냐?
1. 은닉화
2. 변수의 상태를 기억하고 유지
3. 전역변수 사용 억제
나는 클로져를 '생성시점을 기억하는 폐쇄공간'으로 정의하였다.
클로져가 성립하는 조건을 아래와 같이 정리하였다.
클로져는 내부함수가 외부함수의 변수를 참조하며, 함수가 생성됐을 시점의 환경을 기억하는 함수이다.
클로져의 사용용례 몇 개를 가져와 따라해보았다.
+ 버튼을 누르면 위에 숫자가 올라가는 단순한 카운터를 구현하려고 한다.
가장 쉽게 떠올릴 수 있는 방법은 아마 아래와 비슷한 모양이 아닐까?
/* HTML
<h2 id="counter">0</h2>
<button id="counter-btn" style="padding: 5px;">+</button>
*/
var count = 0;
var btn = document.getElementById('counter-btn');
var counter = document.getElementById('counter');
function addCount() {
counter.innerText = ++count;
}
btn.addEventListener('click', addCount);
실제로 동작도 아주 잘 동작한다.
하지만 이 코드엔 문제점이 있는데 바로 '전역 변수'를 사용하고 있다는 점이다.
전역 변수는 어디서나 가져다 사용 할 수 있기 때문에, 만일 다른 코드에 의해 어디선가 count의 값이 바뀌게 된다면 코드가 개발의도대로 작동하지 않을 수도 있다.
어디선가 count를 문자열로 바꿔버렸다면?
난...
카운터였을 뿐이고...
하 하
아무튼 전역변수를 사용하는 것은 예기치 못한 문제를 발생시킬 수 있어, 사용하지않는 것은 권장한다.
그렇다면 전역변수를 사용하지않으면서 count 숫자를 지속적으로 기억하고 있을 수 있는 방법이 있을까?
// 클로저를 사용
var btn = document.getElementById('counter-btn');
var counter = document.getElementById('counter');
// 즉시실행함수(IIFE)는 선언되는 순간 즉시 실행되는 함수를 뜻한다.
// 함수는 실행되며 Lexical Enviroment를 생성한다.
// 클로저는 자신이 '내부함수에서 외부함수의 변수를 참조하며', '생성됐을 때의 환경을 기억'하는 함수이다. 아래의 경우
// * addCount는 선언과 동시에 실행된다.
// 1. 즉시실행함수(addCount)는 함수를 반환하고 바로 소멸한다. 반환된 함수는 '생성됐을 때의 환경을 기억'하며, '외부함수의 변수 count를 참조'하고 있는 클로저이다.
// 2. 변수 count는 클로저에 의해 참조되고 있으므로, 참조되는 동안은 사라지지않고 자신의 상태를 기억하며 외부에서 접근 할 수 없다.
var addCount = (function() { // <- 외부함수
let count = 0; // 외부에서 이 count 변수에 접근할 수 있는가? X = 은닉화에 성공
return function() { // <- 내부함수
counter.innerText = ++count;
}
})();
// console.log(count); count에 접근 불가 (Reference Error)
btn.addEventListener('click', addCount);
이럴 때 클로져가 등장한다.
(일단 즉시실행함수, 렉시컬 환경 등은 설명하지않고 넘어가려한다.)
다시 한번 클로져의 정의를 살펴보자.
클로져는 내부함수가 외부함수의 변수를 참조하며, 함수가 생성됐을 시점의 환경을 기억하는 함수이다.
addCount의 내용을 살펴보면,
내부함수가 외부함수의 변수 count를 참조하고 있는 것을 확인 할 수 있다.
함수가 생성됐을 시점의 환경을 기억한다는건, 쉽게 표현하면 아래와 같다.
이렇게 묶음의 형식으로 기억을 하고 있다는 것이다.
addCount는 함수를 반환함과 동시에 소멸했다.
즉, count라는 변수를 담고 있던 함수가 사라졌음에도 불구하고 내부함수에선 count라는 변수에 접근 할 수 있는 연결다리를 계속 기억하고 있다는 뜻이다.
때문에 함수가 소멸한 이후인 18번째 줄에서는 count라는 값에 접근 할 방법이 전혀 없음에도 불구하고
버튼에 부착된 클로저(addCount)는 지속적으로 count를 증가시킬 수 있는 것이다.
이렇듯 외부에서 더이상 접근할 수 없는 변수와 연결되어있는 '폐쇄공간'을 다루는 것이 바로 클로져라고 할 수 있다.
이렇게 그들만의 공간을 사용하는 클로져를 사용하게 되면 아래와 같은 응용도 가능해진다.
/* ---- 격리공간 ---- */
function makeCounter(cal) {
let count = 0;
return function() {
count = cal(count);
return count;
}
}
/* ---- 격리공간 ---- */
function add(x) {
return ++x;
}
위 코드와 같이 격리공간을 생성하는 makeCounter 함수(클로져)가 있다.
그리고 단순하게 수를 더해주는 add라는 함수가 있을 때
const counter1 = makeCounter(add);
const counter2 = makeCounter(add);
이 코드는 어떻게 작동하게 될까?
makeCounter 함수에 add가 파라미터로 넘어가면서
반환하는 것은 add(count)가 수행되어 나온 count 값을 반환하는 함수이다.
counter1과 counter2 둘 다 동일한 내용을 담고 있을 것이며,
counter1과 counter2 둘 다 동일한 count라는 변수를 다루고 있다.
console.log('counter1');
console.log(counter1());
console.log(counter1());
console.log('counter2');
console.log(counter2());
console.log(counter2());
그렇다면 위 코드를 통해 콘솔에 출력된 값들은 무엇일까?
동일하게 동작하고 동일한 count라는 변수를 다루고 있으니 1,2,3,4가 찍히게 될까?
우리가 앞서 계속해서 '폐쇄공간'으로 다루었듯 각각의 count는 별도의 공간에 존재하게 된다.
때문에 같은 이름으로 선언됐다 할지라도
위 그림과 같은 식으로 존재하게 된다.
// 함수형 프로그래밍에 사용
function makeAdder(x) {
return function(y) {
return x + y;
}
}
const add3 = makeAdder(3);
const add10 = makeAdder(10);
console.log(add3(7)); // 3 + 7
console.log(add10(7)); // 10 + 7
이런 특성을 이용하여 함수형 프로그래밍 패러다임을 따른 개발에 도움을 줄 수도 있다.
이것저것 여러 사용용례를 알아보았지만 클로져의 핵심은 말 뜻 그대로 '폐쇄공간'에 있는 듯 하다.
각자의 폐쇄공간을 가지니, 외부에서 접근 할 수 없는 (은닉화된) 변수를 가지기도 하고
어디선가 (클로져 함수를) 참조하고 있는 한 해당 폐쇄공간은 사라지지않으므로 변수의 상태(값) 또한 그대로 기억하고 있다.
와 클로져!
'프로그래밍 > 정리노트' 카테고리의 다른 글
정리노트: var, let, const (0) | 2022.08.14 |
---|---|
정리노트: 스코프 (0) | 2022.08.13 |
정리노트: 호이스팅 (0) | 2022.08.13 |
정리노트: 브라우저의 렌더링 원리 (0) | 2022.08.13 |
정리노트: Javascript에서 this란 무엇일까? (0) | 2022.05.30 |