
자바스크립트를 공부하다 보면 항상 빠지지 않고 클로저에 대한 얘기가 나온다.
클로저가 무엇이길래 항상 언급되는 걸까
클로저는 주변 상태에 대한 참조와 함께 묶인 함수의 조합이다. 클로저는 내부 함수에서 외부 함수의 범위에 대한 접근을 제공하고, 함수 생성 시마다 클로저는 생성된다. 출처 : mdn - 클로저
여러분들이 만약 map, reduce, filter와 같은 배열 메서드를 사용해본 적이 있다면 클로저를 활용해 본 경험이 있는 것이다.
해당 메서드처럼 함수를 인자로 사용하거나 반환하는 함수를 고차함수라고 한다.
이번에 공부할 개념인 클로저는 고차함수가 함수를 반환할 때 생성되는 대표적인 개념이다.
이렇게 사전적 정의만 보면 이해하기 어려울 수 있다.
간단한 코드를 살펴보자
위의 코드를 실행하게 되면 아래와 같이 함수 별로 다른 카운터 값이 나오는 것을 확인할 수 있다.
이게 가능한 이유가 클로저 때문이다.
counter 라는 함수가 실행되게 되면서 선언 당시 주변 환경을 기억하는 클로저가 생성되게 된다.
const counter1 = counter();
따라서 counter1에는 실행 당시의 렉시컬 환경이 저장되게 되고
외부 함수에서 디폴트 파라미터로 선언된 count = 0를 내부적으로 참조하게 된다.
function counter(count = 0 // 외부 변수를) {
return () => {
return count++; // 내부 함수에서 참조
};
}
따라서 counter1, counter2는 생성 당시 서로 다른 클로저가 생기게 되면서 각자 다른 카운트 값을 관리할 수 있게 되는 것이다.
그러면 내부 함수에서 참조하지 않으면 어떻게 되나요?
당연하지만 함수 생성마다 디폴트 파라미터 값인 0으로 초기화 된 값이 카운터에 저장되게 된다
이러한 클로저는 주로 함수형 프로그래밍에서 활용되게 되는데 커링에 대해서 알아보자
커링은 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수들의 연속으로 변환하는 기법이다.
이것도 사전적인 정의는 어려우니 코드로 이해해보자
예를 들어, 이벤트가 있는데 1등에게는 현재 투입금의 2배, 2등에게는 1,5배, 꽝은 0.5배를 계산해야한다.
function getPrize(multiplier, amount) {
return multiplier * amount;
}
console.log(getPrize(2, 1000)); // 1등: 2000
console.log(getPrize(1.5, 1000)); // 2등: 1500
console.log(getPrize(0.5, 1000)); // 꽝: 500
이렇게 작성해도 되긴 한다. 하지만 여러분의 동료가 코드를 읽는다고 가정해보자
처음 본 사람이 getPrize(1.5, 1000)만 보고 2등에게 주는 금액을 구하는 함수라는 것을 명확히 알 수 있을까?
이 코드를 커링을 사용해서 개선할 수 있다
function getPrize(multiplier) {
return function(amount) {
return multiplier * amount;
};
}
const firstPrize = getPrize(2);
const secondPrize = getPrize(1.5);
const noPrize = getPrize(0.5);
console.log(firstPrize(1000)); // 2000
console.log(secondPrize(1000)); // 1500
console.log(noPrize(1000)); // 500
이렇게 되면 해당 함수를 호출할 때 어떤 것을 구하고 싶은지 명확해진다
아래와 같이 코드를 좀 더 간단하게 작성할 수도 있다.
const getPrize = multiplier => amount => multuplier * amount;
const firstPrize = getPrize(2);
const secondPrize = getPrize(1.5);
const noPrize = getPrize(0.5);
console.log(firstPrize(1000)); // 2000
console.log(secondPrize(1000)); // 1500
console.log(noPrize(1000)); // 500
커링은 위의 예시와 같이 하나의 인자를 고정해두고 싶을 때 사용이 가능하고, 이 때 함수 실행 당시의 렉시컬 스코프를 기억하는 클로저를 이용하여 인자에 접근한다.
클로저라는 개념을 몰랐어도, JS로 코딩을 했다면 여러 곳에서 사용했을 것이다.
하지만 클로저가 어떻게 함수가 종료되어도 외부 변수를 참조할 수 있게 해줄까?
const getPrize = multiplier => amount => multuplier * amount;
const firstPrize = getPrize(2);
이렇게 코드가 있다고 가정해보자
getPrize 함수 객체가 힙에 저장 됨getPrize(2) 호출multiplier = 2가 바인딩 됨amount => multiplier * amount가 힙에 함수 객체로 생성됨multiplier = 2를 클로저로 캡처함multiplier = 2도 힙에 저장됨.getPrize(2)의 실행 결과인 내부 함수 객체를 참조하게 되면서 multiplier = 2도 참조함.위와 같은 흐름으로 클로저가 생기게 된다.
GC 대상에서 제외되면서 메모리 누수가 발생할 수 있고
이것이 클로저의 가장 큰 단점이라고 할 수 있다.
클로저는 다양한 곳에 활용되지만, 캡슐화에도 사용이 가능하다.
IIFE는 즉시 실행 함수로 불리며, 함수를 정의하고 곧바로 호출하는 패턴이다.
예를 들어 카운트 기능이 있다고 가정해보자
const counter = (function () {
let count = 0;
return {
increment() {
return ++count;
},
decrement() {
return --count;
},
getCount() {
return count;
}
};
})();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined
이렇게 선언하자마자 호출을 하게 되면
counter에는 아래의 값이 담기게 된다.
...
return {
increment() {
return ++count;
},
decrement() {
return --count;
},
getCount() {
return count;
}
};
...
이후 외부에서 count에 접근은 불가능하게 되고 increment, getCount 같은 메서드를 통해서만 조작이 가능하게 할 수 있다.
클로저의 개념은 알고 있었지만, 내가 실제로 클로저를 제대로 알고 활용했나 의문이 들 정도로 중요한 개념이었다.