클로저 — 함수가 기억하는 것
Article
12강에서 함수 안의 변수는 함수가 끝나면 사라진다고 했다. 그런데 사라지지 않는 경우가 있다.
function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter());
console.log(counter());
console.log(counter());
- 1
- 2
- 3
makeCounter()는 첫 줄에서 이미 끝났다. 그런데 count는 살아서 값을 기억하고 있다.
이 현상이 클로저다.
왜 살아 있나
12강의 스코프 체인을 떠올려보자. 안쪽 함수는 바깥 변수를 볼 수 있었다.
function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
안쪽의 익명 함수는 count를 쓴다. 즉 자기가 태어난 스코프를 참조하고 있다.
자바스크립트는 누군가 아직 참조하고 있는 변수를 지우지 않는다. makeCounter는 끝났지만, 반환된 함수가 count를 붙잡고 있으니 count는 메모리에 남는다.
- 전역 스코프
- makeCounter 스코프
- 반환된 익명 함수
- makeCounter 스코프
정의는 이렇다. 클로저란 함수와, 그 함수가 선언된 스코프를 함께 묶은 것이다. 함수는 자기가 태어난 환경을 들고 다닌다.
호출할 때마다 새로 만들어진다
makeCounter()를 두 번 부르면 어떻게 될까.
const a = makeCounter();
const b = makeCounter();
console.log(a());
console.log(a());
console.log(b());
- 1
- 2
- 1
b는 1부터 시작한다. 13강에서 배운 대로 함수를 호출할 때마다 새 실행 컨텍스트가 만들어지므로, count도 각자 하나씩 생긴다.
a 와 b 는 각자의 환경을 들고 있다
- a
- 0x1a2b
- 함수 + { count: 2 }
- b
- 0x3c4d
- 함수 + { count: 1 }
서로 간섭하지 않는다. 이 성질이 클로저를 쓸모 있게 만든다.
값을 숨긴다
count를 밖에서 직접 건드릴 수 있을까.
const counter = makeCounter();
console.log(counter.count);
counter.count = 100;
console.log(counter());
- undefined
- 1
건드릴 수 없다. count는 함수 안에 있고, 바깥에서 안은 보이지 않는다. 오직 반환된 함수를 통해서만 값이 바뀐다.
이걸 이용해 "밖에서 함부로 못 바꾸는 값"을 만든다.
function createAccount(initial) {
let balance = initial;
return {
deposit(amount) {
if (amount <= 0) return "금액이 올바르지 않다";
balance += amount;
return balance;
},
getBalance() {
return balance;
},
};
}
const account = createAccount(1000);
console.log(account.deposit(500));
console.log(account.getBalance());
console.log(account.deposit(-9999));
console.log(account.balance);
- 1500
- 1500
- '금액이 올바르지 않다'
- undefined
balance를 음수로 만들 방법이 없다. 반드시 deposit을 거쳐야 하고, 거기서 검사를 한다. 데이터를 지키는 이 방식을 은닉이라 부른다.
그냥 객체
{ balance: 1000 }
obj.balance = -9999가 통한다- 누구나 아무 값이나 넣는다
- 검사 로직을 우회할 수 있다
클로저
함수 안에 가둔 값
- 바깥에서 접근 자체가 불가능
- 정해진 함수로만 바꾼다
- 규칙을 반드시 통과한다
유명한 반복문 함정
12강에서 예고한 그 버그다. var와 반복문과 클로저가 만나면 이렇게 된다.
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
- 3
- 3
- 3
0, 1, 2를 기대했는데 3이 세 번 나온다.
- 01
var i 는 하나뿐이다
블록 스코프가 없으므로 반복문 전체가
i하나를 공유한다. - 02
setTimeout 은 나중에 실행된다
100밀리초 뒤에 실행하라고 예약만 해두고 반복문은 계속 돈다.
- 03
반복문이 먼저 끝난다
i가3이 되어 조건이 거짓이 되고 반복이 종료된다. - 04
이제야 콜백들이 실행된다
세 콜백 모두 같은 `i` 를 본다. 그 값은 이미
3이다.console.log(i); // 세 번 모두 3
세 개의 클로저가 같은 변수 하나를 붙잡고 있었던 게 문제다.
let으로 바꾸면 해결된다.
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
- 0
- 1
- 2
12강에서 말한 "let i는 반복할 때마다 그 회차만의 i를 새로 만든다"가 바로 이 뜻이다. 클로저 세 개가 **각자 다른 i**를 붙잡는다.
세 개의 클로저가 각자의 i 를 들고 있다
- 콜백 1
- 0x0a1
- { i: 0 }
- 콜백 2
- 0x0b2
- { i: 1 }
- 콜백 3
- 0x0c3
- { i: 2 }
실무에서 만나는 클로저
이름을 몰랐을 뿐 이미 쓰고 있었다.
function createGreeter(greeting) {
return name => `${greeting}, ${name}님`;
}
const hello = createGreeter("안녕하세요");
const bye = createGreeter("안녕히 가세요");
console.log(hello("지원"));
console.log(bye("지원"));
- '안녕하세요, 지원님'
- '안녕히 가세요, 지원님'
greeting을 기억하는 함수를 찍어내는 공장이다. 설정을 한 번 넣어두고 계속 쓰는 패턴이라, 라이브러리 설정 함수에서 자주 보인다.
정리하면
- 클로저는 함수와 그 함수가 태어난 스코프를 함께 묶은 것이다. 함수는 자기 환경을 들고 다닌다.
- 바깥 함수가 끝나도, 안쪽 함수가 참조하는 변수는 사라지지 않는다.
- 호출할 때마다 새로운 환경이 만들어져 서로 간섭하지 않는다.
- 값을 함수 안에 가둬 바깥에서 못 건드리게 하는 은닉에 쓴다.
- 반복문 +
var+ 콜백은 같은 변수 하나를 공유해 버그를 만든다.let을 쓰면 회차마다 새 변수가 생겨 해결된다. - 클로저는 메모리를 붙잡는다. 필요 없어지면 참조를 끊는다.
Part 4가 끝났다. 다음 강의부터는 자료구조다. 값 하나가 아니라 여러 값을 한꺼번에 다루는 배열부터 시작한다.