모던자바스크립트 DeepDive : 24장 클로저

te-ing·2022년 4월 22일
0
post-thumbnail

자바스크립트의 클로저는 자신보다 상위스코프에 있는 함수가 종료되었어도 상위스코프에 있던 값을 참조할 수 있도록 만들어주는 자바스크립트의 기능이다.

이러한 방법이 가능한 이유는 내부함수가 생성될 때 상위스코프의 참조를 생성된 함수의 내부 슬롯에 저장하여 참조하고 있기 때문에 가비지콜렉터가 상위스코프를 삭제하지 않아서 이다.

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

렉시컬 환경은 스코프체인으로 상위 렉시컬 환경과 연결되는데, 외부 렉시컬 환경에 대한 참조에 저장하는 것이 상위 렉시컬환경, 상위 스코프이며 함수는 이를 위해 자신의 내부 슬롯에 자신이 정의된 환경 즉, 상위 스코프의 참조를 저장한다.


클로저란?

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

outer() 함수가 호출된 후 outer() 함수의 실행주기가 종료되어 지역변수 const x=10; 또한 생명주기를 마감하였다. 때문에 innerFunc(); 의 실행결과는 전역변수 x = 1; 이어야 하지만 10을 나타내고 있다.

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

위 상황을 자세히 살펴보자면, outer 함수를 호출하면 렉시컬환경이 생성되고 내부슬롯에 전역 렉시컬환경을 할당한다. 그리고 중첩함수 inner가 평가되는데, 이때 중첩함수 inner는 자신의 내부슬롯에 outer 함수의 렉시컬 환경을 상위스코프로서 저장한다. 이후 outer 함수의 실행이 종료되면 inner함수를 반환하면서 생명주기가 제거 된다. 하지만 outer 함수의 렉시컬 환경은 inner함수의 내부슬롯에 의해 참조되고 있고, inner 함수는 innerFunc에 의해 참조되고 있기 때문에 가비지콜렉션의 대상이 되지 않게 된다.


클로저의 활용

캡슐화와 정보은닉

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

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

즉시 실행 함수가 호출되어 반환한 함수가 클로저인 increase 변수에 할당된다. 즉시 실행 함수는 한 번만 실행되므로 num 변수가 초기화 되지 않고, num은 외부에서 접근할 수 없는 private 변수로, 의도치 않은 변경을 막을 수 있다.

이처럼 클로저는 상태가 의도치 않게 변경되지 않도록 은닉하고 특정 함수에게만 상태 변경을 허용하도록 하기 위해 활용되기도 한다. 하지만 자바스크립트는 정보은닉을 완전히 지원하고 있지 않는데, private 필드를 정의할 수 있는 새로운 표준 사양이 ECMA 2022에 구현될 예정이며, 최신 브라우저와 Node.js에는 이미 구현되어 있다.


클로저로 인해 발생하는 실수

var funcs = [];
for (var i = 0; i < 3; i++) { 
	funcs[i] = function () { return i; };
}
for (var j =0; j < funcs.length; j++) {
	console.log(funcs[j]()); // 3 3 3
}

for문의 변수 선언문에서 var 키워드로 선언한 i 변수가 블록 레벨이 아닌 함수 레벨 스코프를 갖기 때문에 전역 변수가 되고, function() { retrun i; }; 에서 이미 3으로 변해버린 전역변수 i를 참조하기 때문에 0 1 2가 아닌 3 3 3이 출력되는 것이다. 이는 var 키워드를 지양해야 하는 이유이기도 한데, 2번째 라인의 var를 let으로 변경하면 i가 for문마다 코드블록의 새로운 렉시컬 환경을 갖게 되어 클로저 문제가 해결된다.

혹은 funcs.forEach(f ⇒ console.log(f())); 와 같이 고차함수를 사용하는 방법도 있다.


커링 + 클로저 구현

function sum(x) {
  return function (y) {
    return function (z) {
      return x + y + z;
    }
  }
}
const sum10 = sum(10); // 10 고정
const sum10and5 = sum10(5); // 10, 5 고정
console.log(sum10(5)(3)) // 18
console.log(sum10and5(3)) // 18
profile
병아리 프론트엔드 개발자🐣

0개의 댓글