클로저란?
클로저란 함수가 선언된 환경의 스코프를 기억하여 함수가 스코프 밖에서 실행될 때에도 기억한 스코프에 접근할 수 있게 만드는 문법이다.
음... 텍스트로는 이해가 어려우니 직관적인 코드로 살펴보자!
function a(name) {
const greet = 'hello, '; // 지역 스코프라 함수가 종료되면 메모리에서 사라짐.
return function () {
console.log(greet + name);
};
};
const world = a("world");
const ik = a("ik");
world(); // hello, world
ik(); // hello, ik
현재 전역 Lexical 환경에는 현재 각각 `a()`과 `world()`, `ik()`이 들어있다.
a의 Lexical 환경에는 `greet: ‘hello, ‘`가 들어있고, a의 블록 안에 존재하는 익명함수 Lexical 환경에는 선언된 것이 없다.
이어서 코드의 결과를 보면, 순서대로 'hello, world'와 'hello, ik'이 출력될 것이다.
왜일까? a() 함수는 내부의 익명함수를 반환하고 생을 마감했기에 a()가 실행된 이후 콜스택에서 제거되었으므로 a()의 변수 'greet'는 분명 가비지 컬렉터에 의해 제거되었기에 접근할 방법이 없을 텐데 말이다.
그 이유는, 클로저로 인해 함수가 선언된 당시 환경의 스코프를 기억하여 wrold = a() 와 ik = a() 각각의 환경에서 생성된 greet가 메모리에서는 사라졌지만, 선언될 당시의 스코프를 기억하여 각각 hello, world와 hello, ik을 출력한 것이다.
위 코드의 Lexical 환경이 참조하는 순서는 다음과 같다.
익명함수 Lexical 환경의 환경 레코드(현재 블록에 해당하는 데이터) -> a Lexical 환경 -> 전역 Lexical 환경
다시 한 번 설명하자면, 클로저는 함수가 선언된 환경의 스코프를 기억하여 함수가 스코프 밖에서 실행될 때에도 기억한 스코프에 접근할 수 있기에 이러한 현상이 가능해지는 것이다.
그럼 제대로 이해했는지 확인하기 위해 다음 코드의 결과에 대해서도 고민을 해보자.
function count() {
let i = 0;
for (i = 0; i < 5; i ++) {
setTimeout(function () {
console.log(i);
}, i * 100);
};
};
count();
예상 답안으로 0, 1, 2, 3, 4가 나올 것이라 예상했다면 아직 클로저에 대해 이해를 제대로 하지 못한 것이다. 답은 5, 5, 5, 5, 5.
위 코드를 살펴보면
- 각각의 setTimeout() 함수는 콜스택에 쌓임.
- 이어서 웹 API중 하나인 타이머가 생성되며, 익명함수는 Web APIs로 이동한다.
- setTimeout()이 완료되고 콜스택에서 제거됨.
- 설정한 타이머가 지난 후 익명함수는 콜백 큐로 이동
- 이 작업이 5번 반복되고, for 루프는 종료 → 콜백 큐에는 5개의 익명함수가 차례로 쌓인 상태
- 루프가 모두 종료되어 현재 콜스택이 비어있는 상태이므로, 이벤트 루프는 콜백큐의 익명함수를 하나씩 콜스택으로 올림.
- 익명함수 실행 → 여기서 익명함수들이 참조해야하는 i는 이미 for 루프가 모두 돌면서 5로 변화된 상태. 따라서 콜백 큐에 있는 익명함수들이 참조하는 i의 값은 5이기 때문에 차례로 콜백큐의 익명함수가 실행 되면서 5가 5번 찍히는 것이다.
그렇가면 i가 순서대로 0, 1, 2, 3, 4가 찍히게 하려면 어떻게 해야할까?
IIFE(즉시 실행 함수)를 사용하여 setTimeout 부분을 괄호로 감싸 즉시 실행 함수 형태로 만들어주면 별도의 스코프를 생성해 해결하는 방법과 let 키워드를 for문을 ()안에 넣어 하나의 블록스코프를 가지고 하나의 반복문 마다 새로운 i를 각각의 익명 함수들이 자신들이 선언된 스코프의 i 값을 참조하게 만드는 것이다.
// 즉시 실행 함수 사용하는 방법
let i = 0;
for (i = 0; i < 5; i++) {
(function(){
var innerI = i;
setTimeout(function timer() {
console.log(innerI);
}, i * 100);
})();
}
// let 키워드 사용하는 방법
for (let i = 0; i < 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 100);
}
그렇다면 클로저는 언제 사용할까?
상태를 안전하게 변경하고 유지하기 위해 사용한다. 즉, 상태가 의도치 않게 변경되지 않도록 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.
클로저는 우리가 흔히 사용하는 React의 useState의 동작 원리로서 사용되는데 이 부분은 추후에 작성하기로 - !
같은 실수를 반복하지 않고, 한 번 학습한 내용을 오래 기억하기 위해 개발하면서 겪었던 트러블 슈팅과 학습한 내용을 정리하고 기록합니다 🧑💻