모던 자바스크립트 Deep Dive 서적을 참고했습니다.
사실 클로저는 자바스크립트 고유의 개념이 아니다.
클로저의 정의가 ECMAScript사양에 등장하지 않는다.
MDN(신뢰할 수 있는 개발자 문서. 공식문서는 아님) 曰: “클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.”
하... 렉시컬은 또 무엇일까? 이왕 하는 김에 쉽고 빠르게 보고 오자. ⇒ 렉시컬 빠르게 집어넣기 클릭
렉시컬 스코프 개념을 이해했다면 술술 읽힐겁니다.
함수는 '태어난 곳'과 '일하는 곳'이 다를 수 있다. 그래서 함수는 '일하는 곳'과 관계없이 자신이 '태어난 곳'의 환경을 기억해야 하는데, 이때 '태어난 곳'(정의 된 위치, 환경)이 바로 상위 스코프이다. => 기억해야 한다.
다시! 상위 스코프란?: 함수의 정의가 위치하는 스코프
이를 위해 함수는 자신의 내부슬롯[[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장한다.
함수의 [[Environment]]가 어떻게 상위 스코프를 결정하는지 보면:
- 상위 스코프 결정 시점
- 함수 정의가 평가될 때(코드가 실행되기 전) 상위 스코프가 결정된다.
- 이것이 바로 '렉시컬 스코프'.
- [[Environment]]의 역할
- 함수가 자신이 태어난(정의된) 환경을 기억하게 해준다.
자바스크립트 deep dive 24-04
const x = 1;
function foo() {
const x = 10;
bar();
}
function bar() {
console.log(x);
}
bar
함수는 전역에서 정의되었으므로 전역 스코프를 기억한다.- 그래서
bar
함수가 어디서 호출되든 상관없이 전역의x
값인 1을 참조한다.foo
안에서 호출되어도 마찬가지이다.
즉, 함수의 상위 스코프는 함수를 어디서 호출하는지가 아니라, 함수를 어디서 정의했는지에 따라 결정된다. 이것이 바로 렉시컬 스코프의 핵심이다.
이 코드하나 머리에 박아놓고 클로저하면 떠올리기
function foo() {
const x = 1;
const y = 2;
function bar() {
console.log(x); // bar는 foo의 x에 접근 가능
}
return bar;
}
const bar = foo(); // foo를 실행하고 bar 함수를 반환받음
bar(); // bar 실행
생명 주기 관점에서 보기
function outer() {
const name = "철수"; // 원래는 outer 함수 종료시 사라져야 할 변수
function inner() {
console.log(name); // 하지만 inner가 name을 기억함
}
return inner; // inner 함수 반환
}
const savedFunc = outer(); // outer는 실행 종료됨
savedFunc(); // 여전히 "철수" 출력 가능!
- 보통의 경우: outer 함수가 끝나면 그 안의 변수들(name)은 사라져야 함
- 클로저의 경우: inner 함수가 name을 참조하고 있어서 사라지지 않고 유지됨
여기에서 의문이 들 수도 있다! (의문 없다면 pass)
“엥?
outer()
함수는 일급객체로써 변수savedFunc
에 넣었기때문에savedFunc();
를 실행하면outer();
가 실행되니까 애초에 이 결과가 당연한거 아니야?” 라고 생각이 들었다면
그렇게 오해할 수 있지만 실제로는 다르게 작동한다.
function outer() {
const name = "박지성";
function inner() {
console.log(name);
}
return inner; // inner 함수를 '반환'
}
// 여기서 잘 보면
const savedFunc = outer(); // (1) outer 함수가 실행되고 inner 함수를 반환
savedFunc(); // (2) 저장된 inner 함수를 실행
const savedFunc = outer()
에서 일어나는 일:
savedFunc()
에서 일어나는 일:
다시 보자면
// 이렇게 생각이 들 수 있는데
const savedFunc = outer; // outer 함수 자체를 저장
savedFunc(); // outer 함수를 실행
// 실제로는 이렇게 동작함
const savedFunc = outer(); // outer()의 반환값(inner 함수)을 저장
savedFunc(); // 저장된 inner 함수를 실행
비유하자면
outer()
는 "조리법"을 주는 게 아니라"일찍 소멸된다"는 의미
function foo() {
const x = 1;
const y = 2;
// 케이스 1: 클로저가 모든 변수 유지
function bar() {
console.log(x, y); // x와 y 모두 필요
}
// 케이스 2: 최적화된 클로저
function optimizedBar() {
console.log(x); // x만 필요
// y는 사용하지 않아서 메모리에서 일찍 제거됨
}
}
이게이게 자바스크립트 엔진의 최적화를 도와준다.
- optimizedBar는 x만 사용하므로 y는 메모리에서 제거
- 불필요한 메모리 점유를 줄임
function counter() {
let count = 0; // 외부에서 직접 접근 불가능한 변수
return {
increase() { count++; },
decrease() { count--; },
getCount() { return count; }
};
}
const myCounter = counter();
myCounter.increase(); // count는 private하게 보호됨
console.log(myCounter.getCount()); // 1
function bigFunction() {
const bigData = new Array(10000); // 큰 데이터
const smallData = "hello"; // 작은 데이터
return function() {
console.log(smallData); // smallData만 사용
// bigData는 사용하지 않으므로 메모리에서 제거됨
};
}
- 최적화 전: bigData와 smallData 모두 메모리 유지
- 최적화 후: smallData만 메모리에 유지, bigData는 일찍 소멸
- 이렇게 클로저는 함수가 자신이 생성된 환경의 변수를 기억하되, 실제로 사용하는 변수만을 메모리에 유지하는 최적화된 방식으로 동작한다.
- 메모리 효율성 & 코드의 캡슐화를 동시 달성!
그래서 자바스크립트에서 클로저가 뭐냐고 물어보면 뭐라고 답해야하지..?!
렉시껄 스코프는 "함수를 어디에 선언했는지"에 따라 상위 스코프를 결정하는
규칙
이다.
const x = 1;
function foo() {
const x = 10;
bar();
}
function bar() {
console.log(x); // 1
}
foo();
클로저는 이 렉시컬 스코프의 규칙을 기반으로, "이미 생명 주기가 끝난 외부 함수의 변수를 참조하는
현상
"이다.
function foo() {
const x = 1;
return function bar() {
console.log(x); // 1
}
}
const savedBar = foo(); // foo는 실행 종료
savedBar(); // 하지만 여전히 x에 접근 가능
즉:
- 렉시컬 스코프: 상위 스코프를 결정하는 규칙
- 클로저: 이 규칙을 활용해 이미 종료된 함수의 변수를 참조하는 현상
- 클로저는 렉시컬 스코프라는 규칙이 있기에 가능한 현상이라고 볼 수 있다.