클로저라는 용어 자체는 생소하게 들릴 수 있지만, 사실 클로저의 내용은 지금까지 자바스크립트를 공부했다면 자연스럽게 알고있는 내용입니다.
코드를 통해 바로 확인해보겠습니다.
function a() {
let a = 1;
function b() {
console.log("a is " + a);
}
return b;
}
let c = a();
c(); // a is 1
a() 내부에 b()가 선언되어 있고, 전역에서 변수 c에 a()를 담아 c()를 실행시키는 모습입니다. 보통의 경우 라인 1의 function a() { ... 부터 라인 9의 let c = a(); 에 이르기까지의 과정에서, a() 는 그 사용을 다 하였기 때문에 가비지 컬렉터가 할당된 메모리를 해제시켜버립니다.
그러나 위의 코드를 직접 실행해보면 마지막 줄의 c(); 에서 콘솔에 "a is 1" 이라는 문구를 정상적으로 출력하게 됩니다. 결국 a() 함수의 직접적인 사용은 라인 9에서 끝이 났지만 (또는 라인 7의 괄호가 닫히기까지), 프로그램은 여전히 a()의 스코프를 기억하고 있다는 의미입니다. 당연하지만, 그 이유는 아래의 c()에 의해 다시 한 번 참조되기 때문입니다.
많은 사람들이 클로저를 설명할 때 자주 예시로 드는 코드가 있습니다.
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, 1000);
}
위 코드를 실행했을때의 결과는 어떻게 될까요?
혹시 1 2 3 4 5 가 출력될거라고 생각하셨나요? 아쉽게도 그렇지 않았습니다.
코드를 실행하면 잠깐의 시간이 흐른 뒤, 6이 다섯 번 출력됩니다. 크롬 콘솔창에 표시되는 원 안쪽의 숫자는 같은 값이 그 숫자만큼 반복되었다는 의미로, 실제로는 6 6 6 6 6 과 같습니다.
어째서 이런 결과가 나오게 되었는지 그 과정을 하나씩 살펴보겠습니다.
첫 반복문의 상황입니다.
> i의 값은 1이고, setTimeout 함수와 함께 timer() 함수가 작동하여 1000ms 뒤, i의 값을 콘솔에 출력하도록 명령합니다.
이어서 두 번째 반복입니다.
> i의 값은 2이고, setTimeout 함수와 함께 timer() 함수가 작동하여 1000ms 뒤, i의 값을 콘솔에 출력하도록 명령합니다.
세 번째 반복도 마찬가지입니다.
> i의 값은 3이고, setTimeout 함수와 함께 timer() 함수가 작동하여 1000ms 뒤, i의 값을 콘솔에 출력하도록 명령합니다.
네 번째, 다섯번째도 i의 값만 바뀔뿐, 같습니다.
그렇다면, 1 2 3 4 5 가 출력되어야 하는게 맞을 것 같은데 결과는 그렇지 않으니 뭔가 이상합니다. 그 원인이 바로 클로저가 현재의 스코프를 기억하기 때문입니다.
setTimeout 함수가 가진 timer가 i 값을 출력하던 아니던, 반복문은 i 값을 증가시키며 계속해서 실행됩니다. setTimeout 함수가 미처 끝나기도 전에 반복문에 사용된 i 값은 이미 다섯번의 반복을 마치고 6이 되어 더 이상의 반복을 진행시키지 않습니다.
결국 글로벌에 선언된 var i 는 6의 값을 가진채로 클로저에게 포착(?) 당해있는 상태이고, setTimeout 함수가 기다리던 1000밀리초가 끝난 뒤 timer 함수가 실행되며 i 값을 출력하는 순서가 됩니다. 이때 참조하는 i 값은 당연히 반복문이 모두 끝난 상태인 i = 6 이 되기 때문에 6을 다섯 번 출력하는 결과를 확인하게 된 것입니다.
해결 방법
의도한대로 동작하지 않아 난감해진 위 상태를 해결하는 방법으로는 크게 두 가지가 있습니다.
첫 번째는 즉시 실행 함수(IIFE) 를 이용하는 방법입니다. 문제가 되는 setTimeout 부분을 괄호로 감싸 즉시 실행 함수 형태로 만들어주면 별도의 스코프가 생성됩니다. 아래와 같이 코드를 수정해봅시다.
for (var i = 1; i <= 5; i++) {
(function(){
var innerI = i;
setTimeout(function timer() {
console.log(innerI);
}, 1000);
})();
}
성공적으로 1 2 3 4 5 가 출력되는 모습입니다.
두 번째 방법은 var을 사용하지 않는 것입니다. 우리는 이미 let 과 const 라는 var 를 대체할 키워드를 알고 있습니다. 두 키워드의 가장 큰 차이점이라면 스코프 블록을 형성한다는 것인데, 이를 이용해 i 의 값들을 분리시키는 방법입니다.
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, 1000);
}
키워드만 let으로 변경했을뿐인데, 정상적으로 동작하는 모습입니다.
지금까지 클로저로 인해 발생하는 문제 상황과 해결법까지 알아보았습니다. 길었던 스코프 시리즈가 마무리 되었네요. 조금 더 심화가 되는 내용을 공부하기 위한 준비가 끝났으니, 이제 진짜 재미있는 내용을 공부할 시간이 된 것 같습니다.
다음은 this에 대한 내용으로 돌아오겠습니다.
'이론 > Frontend' 카테고리의 다른 글
JavaScript의 객체 - 1 (Object of JavaScript) (0) | 2021.08.04 |
---|---|
JavaScript의 this (this of JavaScript) (0) | 2021.07.21 |
JavaScript의 스코프 - 3 (Scope of JavaScript) (0) | 2021.05.13 |
JavaScript의 스코프 - 2 (Scope of JavaScript) (0) | 2021.05.03 |
JavaScript의 스코프 - 1 (Scope of JavaScript) (0) | 2021.04.11 |
댓글