1. 정의
mdn에서는 클로저를 다음과 같이 소개한다. (바로 이해하기 힘든 정의이긴 하다)
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
-클로저는 주위 상태의 참조와 함께 묶인 함수의 조합이다-
이해를 돕기 위해, 쉽게 설명한 클로저의 정의는 다음과 같다.
외부 함수보다 중첩 함수가 더 오래 살아남았을 때, 이미 생명 주기가 다한 외부함수의 요소를 중첩함수가 참조하고 있다면, 이 중첩 함수를 '클로저'라 한다.
2. 예시
2.1. 클로저 요건을 만족하는 경우
foo는 외부함수, bar는 중첩함수 관계로 놓을 수 있다.
foo 함수가 실행되었을 경우, foo 함수는 중첩함수인 bar 함수를 리턴한 뒤 생명 주기를 다하게 된다.
이때 bar 함수는 변수 baz에 할당되며 계속 유지되기 때문에, 외부함수(foo)보다 중첩함수(bar)가 더 오래 유지되는 형태를 띄고 있다.
더 나아가 bar 함수는 foo 함수가 가지고 있는 변수를 참조하고 있으므로, 결과적으로 중첩함수 bar는 클로저의 요건을 만족한다.
function foo() {
const x = 10;
const y = 20;
function bar() {
console.log(x + y);
}
return bar;
}
const baz = foo();
baz(); // 30
2.2. 클로저 요건을 만족하지 않는 경우
2.2.1. 중첩함수가 외부함수의 요소를 참조하지 않을 때
외부함수(foo)보다 중첩함수(bar)가 더 오래 살아남았다.
하지만 중첩함수(bar)가 외부함수(foo)의 요소를 참조하고 있지 않기 때문에, 클로저의 요건을 만족시키지 않는다.
function foo() {
const x = 10;
const y = 20;
function bar() {
const z = 30;
console.log(30);
}
return bar;
}
const baz = foo();
baz(); // 30
2.2.2. 중첩함수가 외부함수보다 먼저 소멸할 때
외부함수(foo)보다 중첩함수(bar)가 먼저 소멸되었기 때문에, 클로저의 요건을 만족시키지 않는다.
function foo() {
const x = 10;
const y = 20;
function bar() {
return x + y;
}
return bar();
}
const baz = foo();
console.log(baz); // 30
3. 클로저의 동작 원리
JavaScript의 Execution Context 및 Garbage Collection 관점에서 생각해볼 수 있다.
본래 JavaScript 엔진은 메모리 관리를 위해 가비지 컬렉션(Garbage Collection, GC) 기능을 제공한다.
즉 코드 상에서 더이상 참조되지 않는 요소는 가비지 컬렉팅의 대상이 되기 때문에 메모리에서 사라진다. (간략화한 표현임)
하지만 앞서 살펴보았듯, 클로저가 되기 위해서는 중첩함수가 외부함수의 요소를 참조하고 있어야 하며, 중첩함수가 외부함수보다 오래 살아남아야 하기 때문에, 결과적으로 클로저가 참조하고 있는 외부함수의 요소는 가비지 컬렉팅 되지 않는다.
앞서 살펴본 아래의 코드를 Execution Context 관점에서 살펴보도록 하자.
function foo() {
const x = 10;
const y = 20;
function bar() {
console.log(x + y);
}
return bar;
}
const baz = foo();
baz(); // 30
가장 먼저 전역 컨텍스트가 call stack에 push된다.
이때의 Execution Context 구조도는 아래와 같다.
이후 foo 함수가 실행된다면, foo 함수의 Execution Context가 call stack에 push된다.
이때의 Execution Context 구조도는 아래와 같다.
foo 함수의 실행이 완료된다면, foo 함수 내부의 bar 함수가 리턴이 되고, foo 함수의 Execution Context는 call stack에서 빠져나간다.
foo 함수가 리턴한 bar 함수는 변수 baz에 할당된다.
이때 foo 함수의 Execution Context가 call stack에 pop 되었더라도, foo 함수의 Lexical Environment는 소멸되지 않는다.
왜냐하면 foo 함수 내부의 변수 x, y는 bar 함수에 의해 참조되고 있으며, bar 함수는 전역 컨텍스트의 변수 baz에 의해 참조되기 때문이다.
즉 가비지 컬렉팅 대상이 아니기 때문에, foo 함수의 Lexical Environment는 소멸되지 않는다.
변수 baz에는 함수 bar가 담기게 된다.
따라서 baz() 명령을 내린다면, 함수 bar의 Execution Context가 call stack에 push되어, 함수 bar가 실행된다.
함수 bar에는 변수 x, y가 존재하지 않으므로, Outer Environment Reference를 통해 foo 함수의 Lexical Environment를 탐색한다.
foo 함수의 Lexical Environment에서는 변수 x, y가 존재하므로 이곳에서 할당된 변수의 값을 사용하게 된다.
4. 클로저의 활용
4.1. 변수 은닉
클로저를 이용하여 함수가 가지고 있는 지역 변수를 은닉함과 동시에 외부에서 활용할 수 있다.
// 생성자 함수를 사용하는 경우
function Counter() {
let value = 0;
this.increase = () => {
return ++value;
};
}
const counter = new Counter();
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.increase()); // 3
console.log(counter.value); // undefined
// IIFE를 사용하는 경우
const counter = (function Counter() {
let value = 0;
return function () {
return ++value;
};
})();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
4.2. React의 useState
React의 useState hook을 구현하는데에도 클로저의 개념이 적용되어있다.
state값을 불러오거나 갱신하는데 클로저가 필요하기 때문이다.
아래 코드는 매우 간단한 수준으로 useState의 동작을 구현한 것이다.
function useState(value) {
let state = value;
function getState() {
return state;
}
function setState(newValue) {
state = newValue;
}
return [getState, setState];
}
const [number, setNumber] = useState(1);
console.log(number()); // 1
setNumber(2);
console.log(number()); // 2
4.3. 커링 함수
커링 함수란 인자를 여러 개 받는 한 개의 함수를, 인자를 한 개씩 받는 함수들을 여러 개 중첩한 형태로 바꾼 것을 뜻한다.
예를 들어 곱셈을 수행하는 함수가 있다고 가정해보자.
아래의 mul 함수는 곱셈을 수행하기 위한 2개의 피연산자를 인자로 받는다.
function mul(a, b) {
return a * b;
}
mul(3, 5); // 15
위의 함수는 두 개의 인자를 받고있는데, 이를 커링 함수로 전환하기 위해서는 한 개의 인자를 받는 함수를 중첩시켜야 한다.
예를 들어 다음과 같이 작성할 수 있다.
const curryMul = (a) => {
return (b) => {
return a * b;
};
};
const double = curryMul(2);
double(4); // 8
curryMul 함수 내부의 익명 함수는 클로저의 요건을 만족한다.
결국 커링 함수는 클로저의 특성을 이용한 것이라 볼 수 있다.
'JavaScript' 카테고리의 다른 글
npm 배포 가이드 및 후일담 (feat. colorpia) (0) | 2024.01.08 |
---|---|
실행 컨텍스트 (Execution Context) (0) | 2023.12.28 |
var, let, const, 호이스팅, TDZ (0) | 2023.12.20 |
프로토타입(Prototype) (0) | 2023.10.07 |
옵셔널 체이닝(Optional Chaining) (1) | 2023.10.04 |