[JavaScript] 클로저(Closures)
클로저(Closures)란?
클로저란 함수가 중첩된 상황에서 내부 함수가 자신이 선언된 어휘적 환경을 기억하고, 외부 함수가 이미 반환이 완료되어 소멸한 상황에서도 내부 함수가 외부 함수에 존재하는 변수를 필요로 한다면 그 변수를 계속 사용할 수 있는 것을 의미한다.
작동 방식
function outer() {
const value = "outer";
function inner() {
console.log(value);
}
inner();
}
outer();
이 예시의 inner
는 자신이 선언된 시점을 기준으로 렉시컬 스코핑이 이루어지기 때문에 상위 스코프인 outer
함수를 탐색하여 value
값을 출력할 수 있다. 즉, 이 코드는 다음과 같은 순서로 작동한다.
outer
함수가 호출된다.inner
함수가 호출된다.inner
함수는 지역 스코프 내에서value
를 탐색하고, 존재하지 않아 탐색에 실패한다.- 렉시컬 스코핑에 따라 선언 시점에서 상위 스코프인
outer
에서value
를 탐색한다. value
를 발견하였으므로"outer"
를 출력한다.
이 코드는 스코프에 대한 기본적인 이해만으로도 충분히 코드 작동을 예상할 수 있다.
그러나,
function outer() {
const value = "outer";
const inner = function () {
console.log(value);
};
return inner;
}
const getInner = outer();
getInner();
이 예시의 경우 outer
함수는 호출되어 inner
함수를 반환하고, 이를 getInner
변수에 할당함에 따라 역할을 마치고 소멸했다. 즉, getInner
에는
function() {console.log(value)}
만 남게 되어 value
라는 값 자체가 유효하지 않은 코드가 할당된 것처럼 보인다. 하지만 실제로 코드를 실행해보면 코드는 다음과 같은 순서로 작동한다.
outer
함수가 호출된다.inner
함수가 반환되어getInner
변수에 할당 된다.getInner
함수가 호출된다.getInner
함수는 지역 스코프 내에서value
를 탐색하고, 존재하지 않아 탐색에 실패한다.getInner
함수에 할당된inner
함수가 선언된 시점의 렉시컬 환경을 기억하여 해당 환경에서 렉시컬 스코핑이 이루어진다.- 기억된 환경에서의 렉시컬 스코핑에 따라 상위 스코프인
outer
에서value
를 탐색한다. value
를 발견하였으므로"outer"
를 출력한다.
이렇듯 getInner
함수가 자신이 생성될 당시에 할당된 inner
함수의 렉시컬 환경을 기억하고, 그 안에서 스코핑이 가능한 것을 클로저라고 부른다.
클로저의 활용
자바스크립트에서 클로저는 다양하게 활용될 수 있으며 이를 통해 효율적인 코드를 작성할 수 있다.
상태 유지
클로저를 활용하면 현재 상태를 기억하고 변경된 최신 상태를 유지할 수 있다. 이는 클로저가 생성될 당시의 렉시컬 환경을 기억하고, 따라서 그 기억된 환경은 소멸되지 않은 채로 최신 상태를 계속해서 유지한다는 점을 활용한 것이다.
예시
토글 버튼을 누를 때마다 문구가 사라졌다가 나타나도록 하는 코드를 작성해볼 수 있다.
<!-- HTML -->
<div class="content">토글 버튼을 눌러보세요.</div>
<button class="toggleButton">토글</button>
// JS
const content = document.querySelector(".content");
const toggleButton = document.querySelector(".toggleButton");
const onToggleClick = (function () {
let isVisible = false;
return function () {
content.style.display = isVisible ? "block" : "none";
isVisible = !isVisible;
};
})();
toggleButton.onclick = onToggleClick;
이 코드의 작동 방식은 다음과 같다.
const onToggleClick = (function () {
let isVisible = false;
return function () {
content.style.display = isVisible ? "block" : "none";
isVisible = !isVisible;
};
})(); // 즉시실행
onToggleClick
변수엔 내부 함수를 즉시 반환하고 소멸하는 함수가 할당되어 있다. 이 때, 반환된 함수는 자신이 생성될 때의 렉시컬 환경을 기억하므로 isVisible
을 기억하는 클로저이다.
toggleButton.onclick = onToggleClick;
onToggleClick
에 할당된 클로저를 toggleButton
의 onclick
이벤트 프로퍼티에 할당했다. 따라서 onclick
에 할당된 클로저는 프로퍼티에서 제거되기 전까지 자신의 렉시컬 환경을 계속해서 기억하기 때문에 isVisible
또한 소멸하지 않고 현재 상태를 기억한다.
const toggleButton = document.querySelector(".toggle");
버튼을 클릭하면 onclick
에 할당된 클로저가 호출된다. isVisible
의 값은 클로저에 의해 참조가 가능하기 때문에 변경이 이루어지고, 렉시컬 환경 속에서 변경된 최신 상태가 계속해서 유지된다.
전역 변수 사용 축소
전역에서 변수를 관리한다는 것은 이 변수에 누구나 접근하고 변경할 수 있다는 의미이다. 따라서 의도치 않은 동작이 이루어질 수 있고 이는 버그로 이어질 수 있는데, 클로저를 활용하면 이러한 전역 변수 사용을 줄일 수 있다.
예시
버튼을 누를 때마다 숫자가 1씩 증가하는 카운터를 만들어 볼 수 있다.
<!-- html -->
<p class="count">0</p>
<button class="increaseButton">+1</button>
// js
const count = document.querySelector(".count");
const increaseButton = document.querySelector(".increaseButton");
let current = 0;
function onIncreaseClick() {
current += 1;
return current;
}
increaseButton.onclick = function () {
count.innerHTML = onIncreaseClick();
};
이 코드의 경우 문제 없이 작동한다. 하지만 current
변수가 전역에 선언되어 있기 때문에 어디에선가 접근이 이루어져 값이 변경될 위험이 높다. 그렇게 될 경우 0부터 시작되어야 할 카운터가 제대로 작동하지 않을 수 있으며, 더한 경우 동작 자체가 불가능한 버그까지 발생할 수 있다.
만약 current
변수를 increase
함수 내부로 옮기고, 클로저를 반환하면 이러한 버그를 방지할 수 있다.
// js
const count = document.querySelector(".count");
const increaseButton = document.querySelector(".increaseButton");
const onIncreaseClick = (function () {
let current = 0;
return function () {
current += 1;
return current;
};
})();
increaseButton.onclick = function () {
count.innerHTML = onIncreaseClick();
};
이 코드의 작동 방식은 다음과 같다.
const onIncreaseClick = (function () {
let current = 0;
return function () {
current += 1;
return current;
};
})();
onIncreaseClick
변수엔 내부 함수를 즉시 반환하고 소멸하는 함수가 할당되어 있다. 이 때, 반환된 함수는 자신이 생성될 때의 렉시컬 환경을 기억하므로 current
를 기억하는 클로저이다.
increaseButton.onclick = function () {
count.innerHTML = onIncreaseClick();
};
increaseButton
의 onclick
프로퍼티에 onIncreaseClick
을 호출하는 이벤트핸들러를 할당하였다. 클로저인 onIncreaseClick
은 호출이 될 때마다 자신이 선언됐을 때의 렉시컬 환경을 기억하여 current
변수를 참조하게 되며 current
변수는 자신을 참조하는 함수가 제거되지 않는 한 계속 존재하게 된다.
const increaseButton = document.querySelector(".increaseButton");
따라서 버튼이 클릭될 때마다 current
변수가 참조 되어 문제 없이 작동하며 외부로부터의 변경에서 안전해졌다.
참고: MDN - Closures
참고: PoiemaWeb