
클로저는 함수와 그 함수 주변의 상태의 주소 조합이다.
즉 클로저는 함수와 그 함수가 접근할 수 있는 변수의 조합이다.
여기까지 보면 이해가 잘 안된다.
예시를 보면 이해하기 쉽다.
const globalVar = '전역변수'; function outer() { const outerVar = 'outer 함수 내 변수'; const inner = function() { return `inner은 ${outerVar}와 ${globalVar}에 접근할 수 있다.`; } return inner; }
위 코드를 보면
outer() 에서는 변수 globalVar 에 접근할 수 있다.inner() 에서는 변수 gloablVar 과 outer() 함수 내부의 outerVar에 접근할 수 있다.즉 위 코드에서 클로저는 두 조합을 찾을 수 있다.
outer() 과 outer() 에서 접근할 수 있는 globalVarinner() 과 inner() 에서 접근할 수 있는 globalVar, outerVar클로저는 변수의 접근 범위를 나타내는 스코프와 비슷하다.
그렇다면 왜 클로저를 구분할까?
클로저의 함수는 어디에서 호출되는지와 무관하게 선언된 함수 주변 환경에 따라 접근할 수 있는 변수가 정해지기 때문이다.
이 말도 이해가 잘 되지 않는다.
예시를 통해 쉽게 이해할 수 있다.
const globalVar = '전역변수'; function outer() { const outerVar = 'outer 함수 내 변수'; const inner = function() { return `inner은 ${outerVar}와 ${globalVar}에 접근할 수 있다.`; } return inner; } const innerOnGlobal = outer(); const message = innerOnGlobal(); console.log(message); // inner은 outer 함수 내 변수와 전역변수에 접근할 수 있다.
위 코드에서 innerOnGlobal은 inner 함수의 주소값을 가진다.
innerOnGlobal은 outer의 밖에 있기 때문에 innerOnGlobal을 호출하면 outerVar에 접근하지 못한다고 생각할 수 있다.
하지만 실제로는 접근이 된다!
그 이유는 inner 함수가 최초 선언되었던 환경에서는 outerVar 에 접근할 수 있었기 때문이다.
innerOnGlobal 은 inner 함수의 주소값을 가지고 있고, inner 함수는 클로저 로서 outerVar에 접근할 수 있다.
이러한 환경을 어휘적 환경(Lexical Environment) 라고 한다.
만약 JavaScript에 클로저라는 개념이 없었다면 innerOnGlobal에서 outerVar에 접근할 수 없어 에러가 발생했을 것이다.
실제로 클로저를 사용할 때는 outer 함수와 inner 함수처럼 함수가 함수를 리턴하는 패턴을 자주 사용한다.
이 때 outer를 외부 함수, inner를 내부 함수라고 통칭한다.
클로저에 대해 학습할 때에는 '외부 함수의 변수에 접근할 수 있는 내부 함수' 등의 표현을 자주 접할 수 있다.
클로저를 활용하면 클로저 함수 내에 데이터를 보존하고 사용할 수 있다.
일반적으로는 함수 내부에 선언한 변수에는 접근할 수 없다.
매개변수도 마찬가지이다.
아래와 같이 함수 내부 변수나 매개변수에 접근하려고 하면 ReferenceError가 발생한다.
function recipe(foodName) { let ingredient1, ingredient2; return `${indredient1} + ${ingredient2} = ${foodName}`; } console.log(ingredient1); // ReferenceError console.log(foodName); //ReferenceError
하지만, 클로저를 활용하면 함수 내부에 선언한 변수와 매개변수에 접근할 수 있다.
기존 함수 내에서 새로운 함수를 리턴하면 클로저로서 활용할 수 있다.
즉, 리턴한 함수의 클로저에 데이터를 보존할 수 있다.
function createRecipe(foodName) { const getRecipe = function(ingredient1, ingredient2) { return `${ingredient1} + ${ingredient2} = ${foodName}`; } return getRecipe; } const juiceRecipe = createRecipe('주스'); juiceRecipe('딸기', '물'); // '딸기 + 물 = 주스' juiceRecipe('당근', '물'); // '당근 + 물 = 주스' juiceRecipe('사과', '물'); // '사과 + 물 = 주스'
위 코드에서는 getRecipe 함수가 클로저로서 foodName, ingredient1, ingredient2 에 접근할 수 있다.
이 때 createRecipe('주스') 로 전달된 문자열 '주스'는 recipe 함수 호출 시 계속 재사용할 수 있다.
createRecipe 가 문자열 '주스'를 보존하고 있기 때문이다.
커링은 여러 전달인자를 가진 함수를 연속적으로 리턴하는 함수로 변경하는 것이다.
function sum(a, b) { return a + b; } function currySum(a) { return function(b) { return a + b; } } console.log(sum(10, 20) === currySum(10)(20)) // true
위의 예시에서 sum 함수는 두 전달인자를 더하는 함수이다.
currySum 함수는 첫 번째 전달인자를 리턴하는 함수로 전달해준다.
sum 과 currySum 이 같은 값을 리턴하기 위해서는 currySum 함수에서 리턴한 함수에 두 번째 전달인자를 전달하여 호출하면 된다.
이렇게 커링을 활용한 함수를 커링 함수 라고 한다.
언뜻 보기에 일반 함수와 커링 함수의 차이가 느껴지지 않지만 커링은 전체 프로세스의 일정 부분까지만 실행하는 경우에 유용하다.
// 커링 함수 function makePancake(powder) { return function (sugar) { return function (pan) { return `팬케이크 완성! 재료: ${powder}, ${sugar} 조리도구: ${pan}`; } } } const addSugar = makePancake('팬케이크가루'); const cookPancake = addSugar('백설탕'); const morningPancake = cookPancake('후라이팬'); // 잠깐 낮잠 자고 일어나서 ... const lunchPancake = cookPancake('후라이팬');
// 일반 함수 function makePancakeAtOnce (powder, sugar, pan) { return `팬케이크 완성! 재료: ${powder}, ${sugar} 조리도구: ${pan}`; } const morningPancake = makePancakeAtOnce('팬케이크가루', '백설탕', '후라이팬') // 잠깐 낮잠 자고 일어나서 만든 팬케이크를 표현할 방법이 없다.
위의 두 예시를 보면 커링은 함수의 일부만 호출하거나, 일부 프로세스가 완료된 상태를 저장하기에 유용하다.
JavaScript에 class 키워드가 없었을 때에는 모듈 패턴을 구현하기 위해 클로저를 사용했다.
모듈은 하나의 기능을 온전히 수행하기 위한 모든 코드를 가지고 있는 코드 모음을 말하며, 하나의 단위로서의 역할을 한다.
모듈은 다른 모듈에 독립적이어야 한다.
즉 모듈은 기능 수행을 위한 모든 기능을 갖추고 있어야 하며, 외부 코드 실행을 통해 모듈의 속성이 훼손받지 않아야 한다.
모듈의 속성을 꼭 변경할 필요가 있는 경우에는 제한적으로 노출된 인터페이스에 의해 변경되어야 한다.
이러한 모듈의 특징은 클로저와 유사하다.
function makeCalculator() { let displayValue = 0; return { add: function(num) { displayValue = displayValue + num; }, subtract: function(num) { displayValue = displayValue - num; }, multiply: function(num) { displayValue = displayValue * num; }, divide: function(num) { displayValue = displayValue / num; }, reset: function() { displayValue = 0; }, display: function() { return displayValue } } } const cal = makeCalculator(); cal.display(); // 0 cal.add(1); cal.display(); // 1 console.log(displayValue) // ReferenceError: displayValue is not defined
위의 예시는 계산기를 모듈 패턴으로 구현한 것이다.
displayValue 는 makeCalculator 코드 블록 외 다른 곳에서는 접근이 불가능하지만 cal의 메서드는 모두 클로저의 함수로서 displayValue 에 접근할 수 있다.
이렇게 데이터를 다른 코드 실행으로부터 보호하는 개념을 정보 은닉(information hiding) 이라고 한다.
이는 캡슐화(encapsulation) 의 특징이기도 하다.
즉 클로저는 특정 데이터를 다른 코드의 실행으로부터 보호해야 할 때 유용하다.
데이터를 보존하는 함수, 커링, 모듈 패턴 으로 활용한다.