클로저란 함수와 그 함수가 선언된 렉시컬 환경(outer - scope chaning) 과의 조합을 말한다.
그럼 이 렉시컬 환경이란 무엇일까?
const x = 1;
function outerFunc() {
const x = 10;
function innerFunc() {
console.log(x); // 10
}
innerFunc();
}
outerFunc();
먼저 각자의 스코프를 살펴보자
const x = 1; 은 전역 스코프
const x = 10; 은 outerFunc() 의 스코프이다.
그럼 console.log(x) 의 x는 이 값을 어디서 참조할까?
먼저 처음에는 이 값을 내부, innerFunc() 에서 참조하는데 이 함수에는 x에 대한 정의가 없다.
해서 그 바로 바깥, outerFunc() 에서 참조한다.
outerFunc() 가 실행될 때의 환경 정보를 참조하는 것이다.
이 환경 정보를 '함수가 선언된 렉시컬 환경', 즉 렉시컬 환경 이라고 부를 수 있다.
쉽게 풀어서, 함수가 선언될 당시의 외부 변수 등에 대한 정보를 말한다.
다른 예시를 하나 더 들어본다.
const x = 1;
// innerFunc()에서는 outerFunc()의 x에 접근할 수 없죠.
// Lexical Scope를 따르는 프로그래밍 언어이기 때문
function outerFunc() {
const x = 10;
innerFunc(); // 1
}
function innerFunc() {
console.log(x); // 1
}
outerFunc();
이 코드의 출력은 '1' 이 나온다.
outerFunc() 를 실행하면 그 안에서 innerFunc() 를 실행한다.
하지만 이 둘의 관계는 서로 다른 스코프에서 정의되었기에, 내부에서 호출하는 관계라고 해도 이는 단순한 호출의 관계일 뿐 스코프에 영향을 미치지 않는다.
스코프를 결정짓는 것은 정의의 위치인 것이다.
이 관계를 '렉시컬 스코프' 라고 한다.
외부 렉시컬 환경에 대한 참조값은 outer, 즉 함수 정의가 평가되는 시점에 결정된다.
다만 하나 더 참고해야 할 점은, 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 여전히 참조할 수 있다는 것이다.
말이 길어서 하나씩 끊어서 다시 설명한다.
1. 외부 함수보다 중첩 함수가 더 오래 유지되는 경우
2. 중첩함수는 종료된 외부 함수의 변수를 참조할 수 있다.
코드로 살펴보자.
const x = 1;
// 1
function outer() {
const x = 10;
const inner = function () {
console.log(x);
};
return inner;
}
//outer() 를 실행 후 innerFunc 에 할당
// = outer() 의 return 을 innerFunc 에 할당
const innerFunc = outer();
// 여기서 outer() 의 실행 컨텍스트는 종료
// 하지만 출력은 10 <- outer 의 실행 컨텍스트를 '여전히' 참조
innerFunc();
코드를 해석해보면, 먼저 innerFunc 변수에 outer() 의 리턴값을 할당하였다.
// (1)
const innerFunc = inner;
// (2)
const innerFunc = function() { console.log(x); }
먼저 1번이라고 설명할 수 있고, 1번은 다시 2번이라고 설명할 수 있다.
따라서 이 innerFunc 안에는 x에 대한 값이 들어가있지 않고, x는 outer() 의 스코프에서 선언되었지만, 이미 outer() 는 종료된 상태이다.
하지만 출력값은 10으로, outer() 의 실행 컨텍스트를 여전히 참조하고 있다.
이 과정이 어떻게 가능한 것일까?
이전에 가비지 컬렉터에 대해 설명하면서, 참조 카운트가 0인 메모리들을 삭제하여 메모리 공간을 확보한다고 했었다.
이 덕분에 실행 컨텍스트가 종료된 상황의 변수들은 삭제되는 것인데, 여기서 outer 함수의 렉시컬 환경은 innerFunc 에서 참조하고 있다.
참조카운트가 0이 아니기에 메모리가 삭제되지 않고, 따라서 x는 10이라는 값을 유지할 수 있는 것이다.
예시 코드들을 몇가지 첨부한다.
function foo() {
const x = 1;
const y = 2;
// 일반적으로 클로저라고 하지 않아요.
function bar() {
const z = 3;
//상위 스코프의 식별자를 참조하지 않기 때문이죠.
console.log(z);
}
return bar;
}
const bar = foo();
bar();
function foo() {
const x = 1;
// bar 함수는 클로저였지만 곧바로 소멸한다.
// 외부로 나가서 따로 호출되는게 아니라, 선언 후 바로
// 실행 + 소멸
// 이러한 함수는 일반적으로 클로저라고 하지 않는다.
function bar() {
debugger;
//상위 스코프의 식별자를 참조한다.
console.log(x);
}
bar();
}
foo();
function foo() {
const x = 1;
const y = 2;
// 클로저의 예
// 중첩 함수 bar는 외부 함수보다 더 오래 유지되며
// 상위 스코프의 식별자를 참조한다.
function bar() {
debugger;
console.log(x);
}
return bar;
}
const bar = foo();
bar();
이 클로저를 왜 이해하고, 왜 사용해야 할까?
일반적으로 메모리의 상태를 안전하게 변경하고 유지하기 위해서 사용한다고 하는데, 이는 즉 의도치 않는 상태 변경을 막기 위해서 라고 말할 수 있다.
// 카운트 상태 변경 함수 #1
// 함수가 호출될 때마다 호출된 횟수를 누적하여 출력하는 카운터를 구현
// 카운트 상태 변수
let num = 0;
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
console.log(increase());
// num = 100; // 치명적인 단점이 있어요.
console.log(increase());
console.log(increase());
출력 : 1 2 3
다만 이 코드에서의 num 변수는 '보호되지 않았기 때문에' 주석의 num = 100; 처럼 외부에서 접근할 위험이 있다.
그럼 어떻게 보완해야 할까?
세가지 단계를 밟아볼 수 있다.
1. 카운트 상태(num 변수) 는 increase() 함수가 호출되기 전까지는 변경되지 않아야 한다.
2. 이를 위해서 num 은 increase() 함수로만 변경할 수 있어야 한다.
=> num 이 전역 변수라서 접근이 가능하니 지역 변수로 바꿔볼까?
이 과정을 먼저 진행해보자.
// 카운트 상태 변경 함수 #2
const increase = function () {
// 카운트 상태 변수
let num = 0;
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
// 이전 상태값을 유지 못함
console.log(increase()); //1
console.log(increase()); //1
console.log(increase()); //1
출력 : 1 1 1
호출할 때마다 num 을 0으로 초기화 하고 ++ 증가를 실행하기 때문에 매번 1이 출력되어 카운트의 의미가 사라진다.
이를 통해 위의 세 단계를 피드백을 해볼 수 있다.
1. num 변수의 외부에서의 변경은 방지하였다.
2. 하지만 increase() 의 호출마다 초기화된다.
이 1번은 유지하되 2번은 수정하는 방향으로 진행하기 위해, 즉 의도치 않은 변경은 방지하면서 이전의 상태값을 유지하기 위해 '클로저'를 사용하는 것이다.
// 카운트 상태 변경 함수 #3
const increase = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저
return function () {
return ++num;
};
})();
// 이전 상태값을 유지
console.log(increase()); //1
console.log(increase()); //2
console.log(increase()); //3
이 코드에서 increase() 함수는
(function () {
// 카운트 상태 변수
let num = 0;
// 클로저
return function () {
return ++num;
};
})
이 과정까지만 수행한다.
위에서 설명한 방식과 동일하게 increase() 를 호출한 곳은 저 return function() { ~ } 을 불러오는 것과 같은 것이다.
이 return 은 클로저에 의해 let num = 0; 을 참조하기 때문에 num 의 값은 유지된다.
출력 : 1 2 3
간단히 설명하자면,
1. 위 코드 실행 시, increase(즉시 실행 함수) 를 호출한다. 이는 increase 변수에 function() { ~ } 이 부분을 할당하는 것이다.
2. 이 할당된 함수는 자신이 정의된 위치에 의해서 결정된 상위 스코프인 즉시 실행 함수, 이 increase 의 렉시컬 환경을 기억하는 클로저 기법이 적용되었다.
즉 let num = 0; 을 기억한다는 것이다.
3. 이후 즉시 실행 함수(increase) 는 소멸한다. callStack 에서 popUp 된다 라고도 말할 수 있다.
따라서 클로저 사용으로 인해 호출마다 초기화되지 않으면서 외부로부터 보호받는 환경을 만들 수 있다.
#12. DOM + Class (1) | 2024.11.18 |
---|---|
#11. 콜백 함수 (Call Back Func) (6) | 2024.11.12 |
#10. 3주차 숙제 (1) | 2024.11.08 |
#9. This (0) | 2024.11.08 |
#8. 실행 컨텍스트 (3) | 2024.11.07 |