클로저

heejung·2022년 4월 13일
0

deep dive

목록 보기
20/20

클로저는 자바스크립트 고유의 개념이 아니므로 클로저의 정의가 ECMAScript 사양에 등장하지 않는다.
MDN에서는 클로저에 대해 다음과 같이 정의하고 있다.

"클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다."


렉시컬 스코프

  • 자바스크립트 엔진은 함수를 어디서 정의했는지에 따라 상위 스코프를 결정
  • 스코프 => 실행 컨텍스트의 렉시컬 환경 => 외부 렉시컬 환경에 대한 참조를 통해 상위 렉시컬 환경과 연결
  • 함수의 상위 스코프 결정 = 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 참조값 결정

함수 객체의 내부 슬롯 [[Environment]]

  • 함수의 상위 스코프의 참조 (함수가 정의된 환경) 를 저장
  • 함수 객체는 상위 스코프를 자신이 존재하는 한 기억함

클로저와 렉시컬 환경

외부 함수보다 중첩 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다.

이러한 중첩 함수를 클로저라고 부른다.

const x = 1;

function outer() {
  	const x = 10;
  	const inner = function() { console.log(x); };
  	return inner;
}

const innerFunc = outer();
innerFunc();
  1. outer 함수 호출 => outer 함수의 렉시컬 환경 생성
  2. outer 함수 객체의 [[Environment]] 내부 슬롯에 전역 렉시컬 환경 저장
  3. 외부 렉시컬 환경에 대한 참조에 [[Environment]] 내부 슬롯에 저장된 값 (전역 렉시컬 환경) 할당
  4. 중첩 함수 inner 평가 (함수 표현식으로 정의 => 런타임에 평가)
  5. inner 함수 객체의 [[Environment]] 내부 슬롯에 outer 함수의 렉시컬 환경 저장
  6. outer 함수가 inner 함수를 반환하면서 생명 주기 종료

이때 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되어도 outer 함수의 렉시컬 환경은 유지된다.
outer 함수의 렉시컬 환경은 inner 함수의 [[Environment]] 내부 슬롯에 의해 참조되고 있고 inner 함수는 전역 변수 innerFunc에 의해 참조되고 있으므로 가비지 컬렉션의 대상이 되지 않기 때문이다.

  1. inner 함수 호출 => inner 함수의 렉시컬 환경 생성
  2. inner 함수 객체의 [[Environment]] 내부 슬롯에 outer 렉시컬 환경 저장
  3. 외부 렉시컬 환경에 대한 참조에 [[Environment]] 내부 슬롯에 저장된 값 (outer 렉시컬 환경) 할당

이처럼 외부 함수보다 더 오래 생존한 중첩 함수는 외부 함수의 생존 여부와 상관없이 자신이 정의된 위치에 의해 결정된 상위 스코프를 기억한다.

자바스크립트의 모든 함수는 상위 스코프를 기억하므로 이론적으로 모든 함수는 클로저지만, 일반적으로 모든 함수를 클로저라고 하지는 않는다.

  • 상위 스코프의 어떤 식별자도 참조하지X => 최적화를 통해 상위 스코프 기억X (메모리 낭비 방지를 위해)
  • 중첩 함수가 외부 함수의 외부로 반환되지X => 중첩 함수가 외부 함수보다 일찍 소멸

위와 같은 경우는 클로저가 아니다.
클로저는 중첩 함수가 상위 스코프의 식별자를 참조하며 외부 함수보다 더 오래 유지되는 경우에 한정한다.

➕➕➕

  • 자유 변수 : 클로저에 의해 참조되는 상위 스코프의 변수
  • 모던 자바스크립트 엔진 => 상위 스코프의 식별자 중 필요한 것만 기억 (최적화 Good) => 메모리 낭비X

클로저의 활용

  • 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용
  • 가변 데이터를 피하고 불변성을 지향하는 함수형 프로그래밍에서 적극 활용
let num = 0;

const increase = function() {
  return ++num;
};

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3

위 코드는 오류를 발생시킬 가능성을 내포하고 있다.
카운트 상태는 전역 변수 num을 통해 관리되므로 언제든지 누구나 접근하여 의도치 않은 변경을 할 수 있다.

const increase = function() {
  let num = 0;
  
  return ++num;
};

console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1

num을 increase 함수의 지역 변수로 변경하여 이제 num은 increase 함수만이 변경할 수 있다.
그러나 increase 함수가 호출될 때마다 지역 변수 num은 다시 선언되고 0으로 초기화되므로 출력 결과는 언제나 1이다.
즉, 상태가 변경되기 이전의 상태를 유지하지 못한다.

이러한 문제는 클로저를 사용하면 해결된다.

const increase = (function() {
  let num = 0;
  
  return function() {
  	return ++num;
  };
}());

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
  1. 즉시 실행 함수 호출 => 반환한 함수 (클로저) increase 변수에 할당
  2. 즉시 실행 함수 소멸 => 클로저는 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억
  3. 클로저 호출 => 카운트 증가

캡슐화와 정보 은닉

  • 캡슐화 : 프로퍼티와 메소드를 하나로 묶는 것
  • 객체의 특정 프로퍼티/메소드를 감추어 정보를 은닉하기 위해 사용
  • 적절치 못한 접근으로부터 객체의 상태 변경을 방지 (정보 보호)
  • 객체 간의 상호 의존성 (결합도) 낮춤

자바스크립트는 정보 은닉을 완전하게 지원하지 않는다.
인스턴스 메소드를 사용하면 자유 변수를 통해 private를 흉내낼 수 있지만, 프로토타입 메소드를 사용하면 이마저도 불가능해진다.
ES6DML Symbol이나 WeakMap을 이용해도 근본적인 해결책이 되지는 않는다.
=> 현재 클래스에 private 필드를 정의할 수 있는 새로운 표준 사양이 제안되어 있는 상태

profile
프론트엔드 공부 기록

0개의 댓글