모던 자바스크립트 Deep Dive 정리
클로저란?
▶ 사전적 의미 : 함수와 그 함수가 선언된 렉시컬 환경과의 조합
▶ 외부 함수보다 더 오래 유지되고 상위 스코프의 식별자를 참조하는 (중첩)함수
▷ 함수가 선언된 렉시컬 환경을 이해하자
const x = 1;
function outerFunc() {
const x = 10;
function innerFunc() {
console.log(x); // 10
}
innerFunc();
}
outerFunc();
▶ 중첩함수 innerFunc는 outerFunc의 내부에서 선언되었기 때문에, outerFunc의 x 변수에 접근 가능
▷ 만약, innerFunc 함수가 outerFunc 함수의 내부에서 정의된 중첩함수가 아니면,
innerFunc 함수를 outerFunc 함수 내부에서 호출한다 하더라도 outerFunc 함수의 변수에 접근할 수 없다
▷ 반례
const x = 1;
function outerFunc() {
const x = 10;
innerFunc();
}
function innerFunc() { // 함수호이스팅 가능
console.log(x); // 1
}
outerFunc();
▶ 이는 자바스크립트가 렉시컬 스코프를 따르기 때문이다
1. 렉시컬 스코프
▶ 렉시컬 스코프 (정적 스코프)
▷ 자바스크립트에서 함수를 어디서 정의했는지에 따라 상위 스코프를 결정하는 것
▷ 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장할 참조값(상위 스코프에 대한 참조) 은
함수가 정의된 환경(위치)에 의해 결정된다. -> 렉시컬 스코프
2. 함수 객체의 내부 슬롯 [[Environment]]
▶ 함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경(상위 스코프의 참조) 을 저장한다
▷ 현재 실행중인 실행 컨텍스트의 렉시컬 환경을 저장한다
(함수 정의가 평가되고 있는 실행 컨텍스트이기 때문)
3. 클로저와 렉시컬 환경
▶ 클로저 예시
const x = 1;
// ①
function outer() {
const x = 10;
const inner = function () { console.log(x); }; // ②
return inner; // 중첩 함수를 호출
}
// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outer(); // ③
innerFunc(); // ④ 10
// 참고로 중첩 함수 inner를 함수 선언문으로 정의해도 결과는 같음
▶ ③에서 outer 함수는 호출되고 중첩함수 inner를 반환하고 종료(생명주기 마감 & 실행 컨텍스트에서 제거)
▷ 따라서 실행 컨텍스트에서 제거되면 outer 함수의 지역 변수은 x = 10;도 생명주기 마감
▷ 그런데 ④에서 10이 출력됨
▶ outer 함수의 반환값인 중첩함수 inner가 innerFunc 변수에 할당되어 유지됨
▷ 또한 outer 함수의 지역 변수인 x = 10; 을 보존 (inner 함수가 outer 스코프에서 선언되었기 때문)
▷ 중첩 함수가 호출되지 않고 남아있다가 외부함수가 리턴해줘서 살아남을 수 있었음
▶ 이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우, 중첩 함수가 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다
▷ 이러한 중첩 함수를 클로저라고 한다
▶ 다시 반복하는 클로저의 정의 (MDN)
▷ 클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다
▶ outer 함수의 실행이 종료될 때, 실행 컨텍스트가 제거되지만, outer 함수의 렉시컬 환경까지 소멸하지 않음
▷ outer 함수의 렉시컬 환경은 inner 함수의 [[Environment]] 내부 슬롯에 의해 참조되고 있고,
inner 함수는 전역 변수인 innerFunc에 의해 참조되고 있기 때문에 가비지 콜렉션의 대상이 되지 않음
4. 클로저의 활용
▶ 클로저 사용 이유
▷ 상태(state)를 안전하게 변경하고 유지하기 위해 사용
▷ 상태가 의도치 않게 변경되지 않도록 상태를 안전하게 은닉하고, 특정 함수에게만 변경을 허용
▶ counter 프로그램을 클로저로 구현하면 (전역변수 사용 X)
const counter = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저인 메서드를 갖는 객체를 반환한다.
// 객체 리터럴은 스코프를 만들지 않는다.
// 따라서 아래 메서드들의 상위 스코프는 즉시 실행 함수의 렉시컬 환경이다.
return {
// num: 0, // 프로퍼티는 public하므로 은닉되지 않는다.
increase() {
return ++num;
},
decrease() {
return num > 0 ? --num : 0;
}
};
}());
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
▶ 즉시실행함수가 클로저를 포함한 객체 리터럴을 반환
▷ 객체 리터럴은 코드 블록이 아니므로 별도의 스코프를 가지지 않음
▶ 객체 안의 increase, decrease 함수는 자신이 정의된 위치이자 상위 스코프인 즉시실행함수의 렉시컬 환경을 기억하는 클로저
▷ 이처럼 클로저는 의도치 않은 상태 변경을 방지하고, (은닉) 특정 함수에게만 상태 변경을 허용
5. 캡슐화와 정보 은닉
▶ 캡슐화
▷ 객체의 상태(프로퍼티)와 동작(메서드)을 하나로 묶는 것
▶ 정보 은닉
▷ 캡슐화를 특정 프로퍼티나 메서드를 감출 목적으로 사용하는 것
▶ 공개할 필요가 없는 프로퍼티나 메서드를 숨겨서 정보 보호
▶ 객체 간의 상호 의존성(결합도)을 낮춰줌
▷ 다른 프로그래밍 언어와 달리 자바스크립트에서는 접근 제한자가 없어 모든 프로퍼티와 메서드가 public임