얼마 전 기술 면접 때 클로저에 대한 질문을 받았는데, 두루뭉술하게만 알고 사용하던 개념이라(심지어 자주 쓰는데도 불구하고...!!!) 명확한 대답을 하지 못했습니다.
기초적인 개념 정리를 위해 글을 적어보도록 합니다...🐒
클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다. -Mdn의 클로저 설명
🤦🤦🤦
스코프란 식별자(변수)의 유효 범위를 말합니다. 함수 밖에서도 참조할 수 있는 글로벌 스코프(전역 변수) 와 함수 내부에서만 접근할 수 있는 로컬 스코프(지역 변수) 가 있습니다.
렉시컬 스코프란 함수의 호출이 아닌 함수가 선언되는 시점에 상위 스코프가 결정되는 것을 의미합니다. (정적 스코프)
const name = 'bangul'; // 전역 변수 name
function cat(){
const name = 'cheese'; // 지역 변수 cheese
meow();
}
function meow(){ // 선언 시 가장 가까운 상위 스코프의 변수를 참조
console.log(`${name}: meow`);
}
cat(); // bangul: meow
meow(); // bangul: meow
함수 meow()
를 선언할 때, 함수 내부의 변수 name
은 자신의 스코프로부터 가장 가까운 상위 스코프부터 계속 변수를 참조하려고 합니다.
meow()
가 선언될 때 가장 가까운 전역 변수 name
을 참조하기 때문에, cat()
함수 안에서 실행된 meow()
함수도 'bangul' 이라는 결과값을 출력해주게 됩니다.
만약 호출된 시점에서 가장 가까운 상위 스코프의 변수를 참조했다면, cat()
을 호출했을 때의 콘솔 로그는 cheese: meow
가 됐겠죠? 🤖
위의 렉시컬 스코프 예제처럼, 자바스크립트에서의 함수는 지역변수와 인자로 받은 매개변수 뿐만 아닌 스코프 외부의 변수에도 접근할 수 있습니다.
const outerFunc = () => {
const name = '빵울';
const innerFunc = () => {
console.log(name);
}
innerFunc();
}
outerFunc(); // '빵울'
함수 innerFunc()
는 outerFunc()
함수 내부에서 선언된 내부 함수입니다.
innerFunc()
안에 정의된 변수는 없으며, console.log
메서드로 name
변수를 호출하고 있습니다.
함수 innerFunc()
는 outerFunc()
함수 내부 선언되었으므로, 렉시컬 스코프를 따라 상위 스코프는 outerFunc()
함수 내부가 되겠네요! 👨🚀
내부함수인 innerFunc()
가 실행될 때,
1. innerFunc()
함수 내부에서 name
변수를 찾습니다.
2. 내부에 해당 변수가 없으면, 상위 스코프에서 name
변수를 찾습니다.
그렇다면 다음 함수는 어떨까요?🤔
const outerFunc = () => {
const name = '빵울';
const innerFunc = () => {
console.log(name);
}
return innerFunc;
}
const myCat = outerFunc(); // const myCat = innerFunc;
myCat(); // 빵울
외부 함수인 outerFunc()
는 지역 변수 name
을 갖고, 내부 함수 innerFunc()
를 선언하고 반환합니다.
변수 myCat
에는 outerFunc()
의 반환값인 innerFunc()
가 담깁니다.
myCat()
을 호출하면 outerFunc()
함수에 정의되어 있던 name
변수를 콘솔로 출력해 줍니다. return
문이 쓰여 함수가 종료되었으니 outerFunc()
내부의 지역 변수에는 더 이상 접근할 수 없을 줄 알았는데, 여전히 outerFunc()
안에서 선언된 name
변수를 참조할 수 있습니다. 🙄
이처럼 자신을 포함하고 있는 외부함수(outerFunc()
)보다 내부함수(innerFunc()
) 가 더 오래 유지되는 경우, 외부 함수 밖에서 내부 함수가 호출되더라도 외부 함수의 지역 변수에 접근할 수 있습니다. 이렇게 상위 스코프가 종료되어도, 상위 스코프의 변수를 참조할 수 있는 함수를 클로저라고 부릅니다.
즉, 자신이 생성될 때의 렉시컬 스코프를 기억하고 있는 함수를 클로저라고 부릅니다.
외부 함수의 실행 컨텍스트가 스택에서 제거가 되어도, 외부 함수의 렉시컬 환경까지 소멸하는 것은 아닙니다.
innerFunc()
의 실행까지 종료되어야 outerFunc()
의 렉시컬 환경까지 소멸됩니다.
클로저를 이용하면 비공개 변수를 만들어 사용할 수 있습니다. 프로그램을 사용할 때 정의해 준 메소드를 사용해서만 비공개 변수를 조작할 수 있습니다.
let num = 0;
const counter = function(){
return ++num;
}
counter();
counter();
counter();
console.log(num); // 3
위 함수는 잘 작동하지만, num
이 전역 변수이기 때문에 언제든지 누구나 접근할 수 있고, counter()
함수가 아닌 다른 조작으로 num
변수의 값을 변경할 수 있어 의도치 않은 상태가 될 수 있습니다.
클로저를 활용하면 안정성 있게 상태 관리가 가능해집니다.
const counter = function(){
let count = 0;
return{
increase: function(){
count++;
},
decrease: function(){
count--;
},
show: function(){
console.log(count);
}
}
}
const myCounter = counter(); // 스코프 체인 생성
myCounter.increase(); // 1
myCounter.show(); // 1
myCounter.increase(); // 2
myCounter.decrease(); // 1
myCounter.show(); // 1
myCounter.decrease(); // 0
myCounter.show(); // 0
counter++; //Uncaught TypeError: Assignment to constant variable.
const counter2 = counter();
counter2.increase();
counter2.show(); // 1
myCounter.show(); // 0
counter()
함수를 호출하는 myCounter
변수에서 클로저와 지역변수 counter
에 담긴 스코프 체인이 생성됩니다. myCounter
의 increase
, decrease
, show
메서드를 호출하면 메서드가 생성된 시점의 상위 스코프counter
변수를 참조할 수 있습니다.
counter
는 지역 변수기 때문에 외부 조작이 불가능하며, myCounter
를 호출할 때 생성된 클로저를 이용해서만 조작이 가능합니다. 이를 통해 의도치 않은 상태 변경을 방지하고 안정성을 증가시킬 수 있습니다.
myCounter
에 할당한 counter()
에서 반환된 클로저와,
counter2
에서 반환된 클로저는 각각 독립된 렉시컬 환경을 갖습니다. 따라서 두 변수 내의 count
변수는 서로 연동되지 않습니다.
클로저는 스코프 체인을 거슬러 올라가서 변수를 찾기 때문에 조금 느리고, 매번 새로운 함수가 리턴되어 다른 메모리 공간을 차지하게 됩니다. 이는 메모리 낭비로 이어질 수 있기에 클로저가 꼭 필요하지 않으면 사용을 자제해야겠죠? 👻