JS에서는 다른 컴퓨터 언어와는 조금 다른 특성을 종종 가지고 있다. 그 중 종종 사용되는 클로저라는 개념에 대해서 알아보자. MDN의 클로저 정의에 따르면, 다음과 같다.
"함수와 함수가 선언된 어휘적(lexical) 환경의 조합을 말한다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다."
여기서 주목할 만한 키워드는 "함수가 선언"
된 "어휘적(lexical)환경"
입니다. 특이하게도 자바스크립트는 함수가 호출되는 환경과 별개로 기존에 선언되어 있던 환경, 즉 어휘적 환경을 기준으로 변수를 조회하려고 한다. 이와 같은 이유로 "외부 함수의 변수에 접근할 수 있는 내부 함수"를 클로저 함수라고 한다.
const adder = x => y => x+y;
adder(5)(7); //12
//화살표 함수 두번 사용.
//함수의 호출(invocation)이 두 번 발생.
typeof adder(5) // = 'function'
adder(5) // y => x+y
// x=5
//리턴 값이 함수의 형태
===
const adder = function(x){
return function(y){ //<-- 리턴 값이 함수의 형태
return x+y;
}
}
클로저는 리턴하는 함수에 의해 스코프(변수의 접근 범위)가 구분된다.
클로저의 핵심은 스코프를 이용해서, 변수의 접근 범위를 닫는(clusure; 폐쇄)데에 있다.
따라서, 함수를 리턴하는 것만큼이나, 변수가 선언된 곳이 중요하다.
const adder = function(x){ // <-- 외부 함수의 변수 x
return function(y){ //<-- 내부 함수의 변수 y
return x+y;
}//<-- 내부 함수
}//<-- 외부 함수
const adder = function(x){
// 외부 함수(adder)의 실행이 끝나더라도, 외부 함수 내 변수(x)를 사용할 수 있다.
return function(y){
return x+y;
}
}
const add5 = adder(5); // 함수 실행이 끝나도, 5라는 값은 사용 가능하다
일반적인 함수는, 함수 실행이 끝나고 나면 함수 내부의 변수를 사용할 수 없다. 이와 다르게, 클로저는 외부 함수의 실행이 끝나더라도, 외부 함수 내 변수가 메모리 상에 저장된다. (어휘적 환경을 메모리에 저장하기 때문에 가능한 일.)
변수 add5 에는 클로저를 통해 리턴한 함수가 담겨있다. add5는 재미있게도, adder 함수에서 인자로 남긴 5라는 값을 x변수에 계속 담은 채로 남아 있다. 외부 함수의 실행이 끝났음에도 말이다.
const adder = function(x){
return function(y){
return x+y;
}
}
const add5 = adder(5);
add5(7) // 12
add5(10) // 15
//즉 adder(5)의 함수 값을 저장해 놓은
add5() === function(y){
return 5 + y}
//와 같이 사용이 가능하다는 의미
const tagMaker = tag => content => `<${tag}>${content}</${tag}>`
const divMaker = tagMaker('div');
divMaker('hello') // = '<div>hello</div>
divMaker('codestates') // = '<div>codestates</div>'
const anchorMaker = tagmMaker('a');
ancorMaker('go') // = <a>go</a>
anchorMaker('urclass') // = <a>urclass</a>
예제에서 divMake
함수는 div
라는 문자열을 tag
라는 변수에 담아두고 있으며, anchorMaker
함수는 a
라는 문자열을 tag
에 담아두고 있습니다.
클로저는 이처럼 특정 데이터를 스코프 안에 가두어 둔 채로 계속 사용할 수 있게 해줍니다.
const makeCounter =() =>{
let value = 0;
return { //*2 함수 여러개를 포함한 객체
increase: () => { //*1 내부 함수 1
value = value +1
},
decrease : ()=>{ //*1 내부 함수 2
value = value -1
},
gerValue: ()=> value //*1 내부 함수 3, 내부 함수를 여러개 만들 수 있다.
}
}
const counter1 = makeCounter();//*2 { increse: f, decrease: f, getValue: f}
다음은 클로저 모듈 패턴
이라고 불리는 아주 유용한 패턴이다.
1) 클로저를 이용해 내부 함수를 단 하나만 리턴하는 것에 그치지 않고, 객체에 담아 여러 개의 내부 함수를 리턴하도록 만든다.
2) makeCounter를 실행하여 변수에 담아보자. makeCounter 함수는 increase, decrease, getValue 매서드를 포함한 객체 하나를 리턴한다. 따라서, counter1은 객체이다.
Q. (makeCounter 함수를 바꾸지 않고)value라는 변수에 값을 새롭게 할당할 수 있을까?
A. X 스코프 규칙에 의해 불가능하다.
외부 스코프에서는 내부 스코프의 변수에 접근할 수 없다 라는 규칙에 의해, 어떤 경우에도 value
는 직접 수정이 불가능합니다. 대신, 리턴하는 개체가 제공하는 메서드를 통해 value
값을 간접적으로 조작 할 수는 있다.
왜 이렇게 하는 것일까? 만일 스코프로 value 값을 감싸지 않았더라면, value 값은 전역 변수여야만 했을 것이다. 하지만 makeCounter라는 함수가 value 값을 보존하고 있기 때문에, 전역 변수로 따로 만들 필요가 없다.
전역 변수가 좋지 않은 이유는, 전역 변수는 다른 함수 혹은 로직 등에 의해 의도되지 않은 변경을 초래하기 때문이다. 이를 side effect라고 한다. side effect를 최소화 하면, 의도되지 않은 변경을 줄일 수 있다. 따라서 이에 따른 오류로부터 보다 안전하게 값을 보호할 수 있다.
클로저를 통해 불필요한 전역 변수 사용을 줄이고, 스코프를 이용해 값을 보다 안전하게 다룰 수 있다.
const counter1 = makeCounter();
counter1.increase();
counter1.increase();
counter1.decrease();
counter1.getValue(); // = 1
const counter2 = makeCounter();
counter2.decrease();
counter2.decrease();
counter2.decrease();
counter2.getValue(); // -3
makeCounter에 의해 리턴된 객체는, makeCounter를 실행할 때에 선언되는 value 값을 각자 독립적으로 가지게 된다. 따라서 conter1에서의 value와 counter2에서의 value는 서로에게 영향을 기치지 않고, 각각의 값을 보존할 수 있다.
클로저를 통해데이터와 메서드를 같이 묶어서 다룰 수 있다. 즉 클로저는 모듈화에 유리하다.