Javascript 공부를 하면 항상 나오는 개념이 클로저(Closure)다. 아는 것 같지만 모르는게 더 많은 거 같은 신비한 클로저에 대해 정리해보고자 한다.
클로저는 Javascript만 사용하는 개념은 아니다. 함수를 일급 함수로 다루는 언어 혹은 런타임에 함수를 생성하는 언어에서 발생하는 현상을 이용한 개념이다.
namespace DisplayConstants
{
class Program
{
private int num;
static void Main(string[] args)
{
double num1 = 10.9;
double num2 = 52.16;
Console.WriteLine("일반 덧셈 결과: ", (num1 + num2));
double num3 = 10.9;
int num4 = (int)num3;
Console.WriteLine(num4);
}
}
}
위 코드는 C#코드다. C#은 정적으로 메소드를 분석하여 컴파일 타임에 메모리 공간을 잡아둔다고 한다. 즉, 컴파일 단계에서 메소드가 접근할 수 있는 변수들을 계산해둔다. 하지만 Javscript는 다르다.
const outer = function() {
let a = 1;
const inner = function() {
return ++a;
}
return inner;
}
const outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3
Javascript에서 함수는 함수를 값으로 취급하는 일급함수이다. 또한, 함수를 런타임에 생성도 가능하다. 즉, 정적으로 분석할 수 없기 때문에 실행 컨텍스트 방식을 이용한다.
위 코드에서 스코프는 전역 스코프, outer
함수 내 스코프, inner
함수 내 스코프가 있고, inner
함수 내에 있는 변수 a
는 상위 스코프(outer
)에 있는 a
변수를 참조하고 있다. outer
함수는 inner
함수를 외부로 반환하여 outer2
에 할당하여 호출하고 있다.
outer
스코프 내에 있는 변수를 참조하는 inner
함수를 외부로 노출하면서 outer
함수 내에 a
에 대한 GC에 의해 수거가 불가능해진 것이다.(a
참조 카운트가 생김) 이 상황이 클로저다.
위 상황을 좀 더 일반적으로 정의하자면, 외부 스코프에 존재하는 자유 변수(a
)가 내부 스코프에 의해 제거되지 않고, 갇혀진(closure)현상을 일컫는 말이다.
클로저가 발생하면 메모리가 GC에 의해 해제되지 않기 때문에 관리 해줘야 한다. 당연히 GC가 되지 않기에 GC에 의해 수거될 수 있도록 참조 카운트를 0으로 만들어야 한다. 이 때 사용할 수 있는 방법이 null
을 할당하는 방법이다.
const outer = function() {
let a = 1;
const inner = function() {
return ++a;
}
return inner;
}
const outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3
outer2 = null; // GC에 의해 수거됨
“외부 스코프의 변수를 잡아두고, 이를 외부에 노출한다.”라는 점이 클로저의 특징이다. 이를 조금 더 생각해보면 외부에 대한 내부의 노출을 어디까지 허용할 것인가를 의미하기도 한다. 즉, 접근 제어의 특징으로도 활용할 수 있다. 이 두가지 특성을 이용한 활용 방법에 대해 알아보자.
const fruits = ["🍎","🍊","🍓","🍑"];
const ulEl = document.createElement("ul");
// 무명함수 콜백
fruits.forEach(fruit => {
const liEl = document.createElement('li')
liEl.innerText=fruit
liEl.addEventListener('click', () => {
alert(`${fruit} click`);
})
ulEl.appendChild(liEl)
})
document.body.appendChild(ulEl)
ul
태그를 만들고 li
태그를 생성하여 과일들을 나열하고 각 li
태그에 eventListener를 달아서 alert창을 띄우도록 하는 코드다.
위 코드에서 li
태그를 선택하면 작성한대로 alert창이 뜬다. 하지만, 과일창을 alert창 기능을 따로 분리하고 싶어서 따로 빼서 콜백으로 전달한다면 문제가 발생한다.
const fruits = ["🍎","🍊","🍓","🍑"];
const alertFruit = (fruit) => {
alert(`${fruit} click`);
}
// callback 함수 빼서 전달 방식 => 문제있음
fruits.forEach(fruit => {
const liEl = document.createElement('li')
liEl.innerText=fruit
liEl.addEventListener('click', alertFruit)
ulEl.appendChild(liEl)
})
document.body.appendChild(ulEl)
위 코드를 실행하면 [object PointerEvent] click
가 발생한다. 이유는 addEventListener
의 콜백(callback)의 첫번째 인자는 Event객체를 전달하기 때문이다. 여기서 클로저를 활용한다면 정상적으로 콜백을 따로 뺄 수 있게 된다.
const fruits = ["🍎","🍊","🍓","🍑"];
const alertFruit = (fruit) => {
return () => {
alert(`${fruit} click`);
}
}
fruits.forEach(fruit => {
const liEl = document.createElement('li')
liEl.innerText=fruit
liEl.addEventListener('click', alertFruit(fruit))
ulEl.appendChild(liEl)
})
document.body.appendChild(ulEl)
위 코드는 alertFruit을 통해 fruit
을 전달하고 리턴을 하는 함수를 만들어 addEventListener
의 콜백으로 전달하여 alert창을 띄우도록 하였다. 이 코드는 정상적으로 작동한다.
콜백함수를 전달하기 전에 자유변수 fruit
을 저장해두고 콜백 함수를 실행 시켰다는 점이 클로저를 활용한 부분이 된다.
정보 은닉은 어떤 모듈의 내부 로직에 대한 외부로의 노출을 최소화하여 모듈의 결합도를 낮추는 방식이다. Javascript에서는 클로저를 활용하여 외부에 대한 노출을 제한할 수 있다.
const Car = {
fuel: 100,
consumeOnce: 20,
<run() {
console.log('run')
this.fuel = this.fuel - this.consumeOnce;
console.log(`remain fuel: ${this.fuel}`)
}
}
Car.run()
console.log(Car.fuel) // 80
Car.run()
console.log(Car.fuel) // 60
Car.fuel = 100; // 외부에서 차의 연료 값을 변경하였다. 외부와 Car는 강결합
console.log(Car.fuel) // 100
위 코드의 문제는 Car.fuel
의 변경을 외부에서 자유롭게 할 수 있다는 것이다. 이렇게 되면 강결합이 일어나 유지보수가 어렵게 된다. 클로저를 통해서 외부에서는 run
메소드만 접근하도록 제한할 수 있다.
const createCar = function () {
let fuel = 100;
const consumeOnce = 20;
return Object.freeze({
get fuel () {
return fuel;
},
run: function () {
fuel = fuel - consumeOnce;
console.log(`remain fuel: ${fuel}`)
}
})
}
const Car = createCar();
Car.run()
console.log(Car.fuel) // 80
Car.run()
console.log(Car.fuel) // 60
Car.fuel = 100;
console.log(Car.fuel) // 60
createCar
함수를 이용하여 Car
객체를 만들었다. fuel
를 수정해도 Car.fuel
은 변경되지 않는다. 외부에서 내부를 변경하지 못하게 하여 결합도를 낮췄다.
부분 적용 함수는 일부 인자를 미리 넘겨 놓고 추가적인 인자를 받아서 실행시키는 함수를 말한다. 이 때도 자유변수를 기억해두는 클로저의 특징을 이용하여 구현할 수 있다.
const debounce = (eventName, func, wait) => {
let timeoutId = null;
return (event) => {
const self = this;
console.log(eventName, 'event 발생');
clearTimeout(timeoutId);
timeoutId = setTimeout(func.bind(self, event), wait);
}
}
const moveHandler = (e) => {
console.log('move event 처리')
}
const wheelHander = (e) => {
console.log('wheel event 처리')
}
document.body.addEventListener('mousemove', debounce("mousemove", moveHandler, 500))
document.body.addEventListener('wheel', debounce("wheel", wheelHander, 500))
debounce
함수를 구현한 코드다. debounce
함수의 eventName
, func
, wait
등을 미리 자유변수로 잡아두고 addEventListener
의 콜백에 넘기는 방식이다.
커링함수는 여러개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠 순차적으로 받는 함수를 말한다. 이 때도 순차적으로 인자를 받다가 모든 인자를 받으면 계산이 실행된다. 이처럼 지연 실행에 사용되는 커링함수를 클로저를 사용하여 구현할 수 있다.
const getInformation = baseUrl => path => id => fetch(`${baseUrl}${path}/${id}`);
const imageUrl = 'http://imageUrl.com';
const getImage = getInformation(imageUrl);
const getIcon = getImage('icon')
const getProfile = getImage('profile');
const user1Profile = getProfile(1);
const user2Profile = getProfile(2);
const icon1 = getIcon(1);
const icon2 = getIcon(2);
기존 데이터를 가지고 있는 클로저의 특성을 이용하여 커링함수를 만들고 이를 통해 URL 접근에 대한 중복을 줄일 수 있다.
클로저의 특징은 다음과 같다.
언어적 특징인 클로저를 잘 활용하는 것이 무엇보다 중요할 것 같다.
참고
코어 자바스크립트 - 정재남
코드스피츠