본문 바로가기
개발서적/모던 자바스크립트 Deep Dive

클로저 (모던 자바스크립트 Deep Dive)

by 사과넹 2024. 4. 16.
반응형

  • 함수형 프로그래밍 언어에서 사용되는 중요한 특성이며 자바스크립트 고유의 개념은 아니다.

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

 

24.1 렉시컬 스코프

  • 함수가 어디서 호출되었는지가 중요하지 않고, 함수가 어디에서 정의되었는지에 따라 상위 스코프를 결정한다.
    • 이것이 렉시컬 스코프이다.
  • 스코프의 실체는 실행 컨텍스트의 렉시컬 환경이고,
  • 렉시컬 환경은 자신의 외부 렉시컬 환경에 대한 참조를 통해 상위 렉시컬 환경과 연결되고,
  • 이것은 스코프 체인이며
  • 외부 렉시컬 환경에 대한 참조는 함수가 정의된 위치에 의해 결정된다.

 

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

  • 함수가 정의되어 평가되는 시점에서 상위 스코프를 기억해야한다.
    • 이것이 내부 슬롯 [[Environment]]에 상위 스코프의 참조를 저장한다.
  • 평가되는 시점이란, 함수가 호출되는 시점이 아닌 해당 함수가 정의된 스코프의 코드가 평가되는 시점에 함수의 객체가 생성되어 내부 슬롯인 [[Environment]]에 평가되고 있는 스코프가 저장된다.

 

24.3 클로저와 렉시컬 환경

const x = 1;

function outer() {
	const x = 10;
  // inner 함수의 외부 렉시컬 환경에 대한 참조는 outer 함수 스코프이다.
	const inner = function () { console.log(x); };
	return inner;
}

const innerFunc = outer();
// inner 함수를 반환하며 생명주기 끝났으며
// 이 함수가 가지고 있던 지역변수 x는 유효하지 않게 되었다.
// = outer 함수의 실행 컨텍스트가 pop되어 사라졌다는 뜻이다.
innerFunc(); // 10 outer 함수의 실행컨텍스트는 pop되어 없는데 지역 변수 x의 값이 나타나고 있다.
  • 즉, 위의 현상은 참조하고 있는 외부 함수의 실행 컨텍스트가 사라졌는데도 불구하고, 중첩 함수가 더 오래 살아있어 외부 렉시컬 환경의 변수를 참조를 하고 있는 상황인거다.
    • 이런 중첩 함수를 클로저라고 부른다.
  • 실행 컨텍스트에서 pop되어 스택에서 제거되었다 한들, 해당 함수의 렉시컬 환경까지 소멸하는 것은 아니기 때문에 참조는 가능하다.
    • 왜냐면 inner함수의 내부 슬롯인 [[Environment]]에 외부 렉시컬 환경에 대한 참조에 이미 outer 함수의 렉시컬 환경이 할당되어 있기 때문이다.
    • 이런 경우, 가비지 컬렉터는 누군가 참조하고 있는 렉시컬 환경에 대한 메모리 공간을 해제하지 않는다.
  • 자바스크립트의 모든 함수는 상위 스코프를 기억하기 때문에 이론적으로 모든 함수는 클로저이지만 일반적으로는 그렇게 부르지 않는다.
  • 다음은 클로저가 아닌 경우이다.
    • 중첩 함수가 외부 함수보다 더 오래 유지되었다 한 들, 외부 함수의 식별자를 참조하지 않는 경우에는 브라우저는 최적화를 통해 상위 스코프를 기억하지 않는다.
    • 중첩 함수가 외부 함수의 식별자를 참조하고 있지만 외부 함수보다 빨리 소멸된다면 클로저라고 하지 않는다.
  • 따라서 클로저의 조건은 아래와 같다.
    • 중첩 함수가 상위 스코프의 식별자를 참조하고 있어야 한다.
    • 중첩 함수가 외부 함수보다 더 오래 유지되어야 한다.
  • 클로저가 참조되는 상위 스코프의 변수는 자유 변수라고 부른다.
    • 여기서 클로저의 명명 의의가 나오는데, 클로저란 함수가 자유 변수에 대해 닫혀있다라고 하며 다른 말로 자유 변수에 의해 묶여있는 함수라고 한다.

 

24.4 클로저의 활용

  • 클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다.
  • 의도치 않은 상태 변경을 막기 위해 상태를 은닉하고 특정 함수에게만 상태 변경을 허용한다.
const increase = (function () {
	let num = 0;
	
	return function () {
		return num++;
	};
}());

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
  • 위의 예제에서 num이 계속 초기화되지 않는 이유는 즉시 실행 함수의 특징때문이다.
    • 즉시 실행 함수는 정의되는 순간 호출되며 더 이상 호출되지 않는다. 즉 소멸한다.
  • 이미 소멸된 즉시 실행 함수를 계속 호출할 수 있는 이유는 아직 유지가 되고 있는 중첩 함수때문이다.
    • 중첩 함수는 본인이 정의되었을 때, 외부 렉시컬 환경에 대한 참조에 외부 함수의 렉시컬 환경을 저장하고 있기 때문에 num에 대한 정보를 알고 있고, 참조하고 있는 렉시컬 환경의 외부 함수는 소멸했기 때문에 식별자에 대한 초기화는 하지 않으나 환경만 빌려줄 뿐이다.
  • 이런 식으로 클로저를 잘 활용하면 의도치 않은 상태 변경을 막기 위해 식별자를 은닉하고, 클로저 함수만 상태를 변경할 수 있게 할 수 있다.
  • 생성자 함수에서 클로저를 사용한 예시는 아래와 같다.
const Counter = (function () {
  // 자유 변수, 생성자 함수의 정적 프로퍼티 => 인스턴스에게 상속 불가
	let num = 0;
	
	// 생성자함수의 프로토타입 함수 => 인스턴스에게 상속 가능
	Counter.prototype.increase = function () {
		return ++num;
	};
	
	Counter.prototype.decrease = function () {
		return num > 0 ? --num : 0;
	};

	return Counter;
}());

 

  • 함수형 프로그래밍에서 사용하는 클로저의 예시는 아래와 같다.
function makeCounter(aux) {
	let counter = 0;
	
  // 클로저
	return function () {
		// 인수를 통해 보조 함수에게 상태 변경을 위임한다.
		counter = aux(counter);
		return counter;
	};
}

// 보조함수
function increase(n) {
	return ++n;
}
// 보조함수
function decrease(n) {
	return --n;
}

const increaser = makeCounter(increase);
const decreaser = makeCounter(decrease);
  • 이 경우에 makeCounter함수는 계속 호출할 수 있는 상태이기 때문에 increase와 decrease함수는 자유변수를 공유하는 상태는 아니다. 다른 렉시컬 환경을 참조하게 된다.
  • 이는 즉시 실행 함수 형태로 변경하여 렉시컬 환경을 공유하게끔 해야한다.

 

24.5 캡슐화와 정보 은닉

  • 캡슐화?
    • 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 메서드의 조합
    • 이는 은닉이라고 표현된다.
    • 캡슐화를 통해 결합도를 낮출 수 있다. 외부에서 객체의 상태를 조작할 수 없도록 하기 위해서.
  • 자바스크립트에서는 접근 제한자를 제공하지 않기 때문에 기본적으로 객체의 모든 프로퍼티와 메서드는 public하다.
  • 아래는 자바스크립트로 캡슐화를 구현해본 예시이다.
const Person = (function () {
	let _age = 0;

	function Person(name, age) {
		this.name = name;
		_age = age;
	}

	Person.prototype.sayHi = function () {
		console.log(`Hi! My name is ${this.name}. I am ${_age}`);
	};

	return Person;
}());

const me = new Person('Lee', 20);
me.sayHi(); // Hi! My name is Lee. I am 20
const you = new Person('Kim', 30);
you.sayHi(); // Hi! My name is Kim. I am 30

// _age값이 유지되지 않는다.
me.sayHi(); // Hi! My name is Lee. I am 30
  • 마지막 _age의 값은 가장 최근 할당된 변수로 변경되었다.
    • 그 말은 즉슨, me와 you 인스턴스는 동일한 렉시컬 환경을 바라보고 있다는 말이다.
      • 이유는 즉시 실행 함수는 정의되는 동시에 단 한번 실행되기 때문에 인스턴스 생성시마다 새로운 렉시컬 환경을 만들지 않는다.
  • 이런 이유 때문에 인스턴스끼리 자유 변수의 값이 공유되어 하나의 인스턴스 상태 유지가 어렵다.
  • 그러므로 자바스크립트는 완벽한 정보 은닉을 지원하지 않는다.
  • 최신 브라우저와 Node.js 12 이상에서는 private 필드를 정의할 수 있도록 지원한다고 한다.

 

24.6 자주 발생하는 실수

  • 함수 레벨 스코프만 지원하는 var는 사용하지 말자.
    • 반복문이 동작할 때마다 새로운 렉시컬 환경을 생성하지 않기 때문에 혼동을 준다.
  • 블록 레벨 스코프를 지원하는 let과 const를 사용하면 예상한대로 반복문이 동작한다.
    • 반복문이 시작할 때마다 새로운 렉시컬 환경을 생성하며 식별자 값을 유지한다.
728x90
반응형