자바스크립트 클로저 (Closure)

김준하·2021년 9월 24일

Youtube 체널 Fireship.io 의 in 100 seconds 시리즈 중 Closer의 내용을 번역 및 요약했습니다.
원본은 다음 영상을 참고해 주세요.

https://www.youtube.com/watch?v=vKJpN5FAeF4


클로저란?

간단하게 요약하면, 클로저는 자바스크립트 함수에서 변수를 참조할 때 자신의 로컬 스코프 밖에서 변수를 참조하는 것 (중괄호 밖의 변수를 사용하는 것)이라고 할 수 있습니다.

let b = 3;
function addBar(a) {
  	// 이때 b는 addBar 함수 내부에 선언되어있지 않고 외부에서 참조
	return a + b;
}

자바스크립트가 이와 같은 동작을 수행하기 위해 자바스크립트 인터프리터는 함수와 함수가 의존하는 주변 환경들(데이터)의 연관관계를 알아야 합니다. 따라서, '클로저(Closure)'는 자바스크립트 엔진이 해당 정보를 수행하기위해 함수와 의존하는 환경을 박스로 포장해서 엔진으로 넘겨주는 단위라고 생각할 수 있습니다.

닫힌 구조의 함수와 열린구조의 함수

예를 들어 아래 함수는 함수 자체 지역 스코프 변수와 파라미터에만 의존한다고 할 수 있습니다.

function addFoo(a, b) {
	return a + b;
}
// 함수가 참조하는 외부로부터의 데이터가 없음.

좀 더 상세하게, 해당 함수는 외부적인 변수의 참조가 없는 자립적인 닫힌 구조이기 때문에 함수가 호출되면 콜 스텍 에 쌓이고, 함수의 데이터는 함수가 콜 스텍에서 사라질 때 까지만 메모리(Stack Memory)에 보전됩니다.

하지만 아래 함수처럼 지역 스코프 밖의 영역인 전역 환경이나 외부 함수로부터 데이터를 참조하는 함수는 어떻게 실행될까요?

let b = 3;
function addBar(a) {
  	// 이때 b는 addBar 함수 내부에 선언되어있지 않고 외부에서 참조
	return a + b;
}

이 함수는 b 변수에 종속적인 열린 구조의 함수로써, 인터프리터가 함수를 수행하면서 의존하는 변수(b)의 값을 알기 위해서 클로저를 생성해 나중에 참조가능한 메모리 영역에 보관합니다. 이 메모리 영역을 힙(Heap) 영역이라고 부르며, 콜스텍과는 달리 데이터가 정리될 시점을 특정하지 않고 보관하며 나중에 가비지 콜랙터에 의해 정리됩니다.

따라서 클로저는 함수 자체가 아니고 함수가 외부의 환경과 결합한 상태를 의미하며, 이 상태는 Lexical Environment 라고도 불립니다.

당연히 클로저는 닫힌 구조의 함수보다는 더 많은 메모리와 프로세싱 능력을 요구하지만, 사용될 수 밖에 없는 이유가 있습니다.

데이터캡슐화(Encapsulation) 와 클로저

한가지 이유는 데이터 캡슐화(Encapsulation)인데, 데이터가 불필요한 곳에서 노출되거나 새어나가지 않도록 보호하는 의미가 있습니다.
아래 코드를 예시로 보자면, 내부함수는 외부함수의 환경을 참조할 수 있지만, 외부함수는 내부함수의 환경을 참조할 수 없기 때문에 외부함수의 지역 변수로 state 를 선언하고 내부함수가 state 를 참조하는 구조는 데이터가 외부 환경으로 노출되는 것을 막을 수 있는 자립적인(self-contained) 구조라고 할 수 있습니다.

function outer() {
	let state = 'foo';
  	function inner() {
    	return `hello ${state}`;
    }
  	return inner;
}

많은 자바스크립트 API 는 Callback 구조로 이루어져 있는데, 클로저를 사용하여 파라미터를 받아 응용하는 함수를 반환하는 '함수 팩토리' 함수를 만들어서 콜백을 요구하는 다른 함수에 전달하는 식으로 사용되어 질 수 있습니다.

function alertFun(message) {
  	return () => {
    	alert(`⚠️ ${message}`);
    };
}

const alertMom = alertFun('hi mom');
alertMom();

클로저를 모른다면 까다로울 수 있는 기술면접 질문

이 함수의 실행결과는 어떻게 될까요?

for(var i = 0; i < 3; i++) {
	const log = () => {
    	console.log(i);
    }
    setTimeout(log, 100);
}

코드의 진행을 다음 순서처럼 요약해 볼 수 있습니다.

  1. 전역변수 i 가 for 루프를 통해 3번 반복되는 동안 log 함수는 closure 를 통해 i 라는 외부 변수를 참조한다.
  2. 100ms 후 log 함수가 setTimeout 에 의해 실행된다.

이 함수가 실행되면 그 결과는 무엇일까요?

변수 i 는 매 루프의 반복마다 클로저를 통해 보관되기 때문에, 함수의 결과를 0,1,2 로 예측하기 쉽습니다. 하지만 실제로 함수를 실행해 보면 3,3,3 이라는 결과를 얻게 될 것입니다. 결과를 정확히 이해하기 위해서는 varlet 으로 선언된 변수의 차이가 클로저에 미치는 영향을 생각해 보아야 합니다. var 를 통해 선언되어진 변수는 부모 환경 스코프로 딸려가버리는 특징이 있는데, 위의 경우에서 부모는 전역 스코프(global) 입니다. 따라서, 지역스코프에 변수를 바인딩 하기 위해서, var 선언을 let 으로 바꾼다면 원래 우리의 예측인 0,1,2 가 출력됩니다. 이를 통해 var 로 선언된 변수는 매 루프마다 하나의 변수 참조가 계속 변경되는 반면, let 으로 선언된 변수는 루프의 반복마다 새로운 변수 참조를 만들어 내는 것으로 보여지고, 이렇게 생성된 변수 참조는 for 루프에 지역적이기 때문에 외부에서 참조되어질 수 없다는 특징이 있습니다.

만약 자바스크립트에 클로저가 없었더라면, 자바스크립트는 변수 i 를 콜스텍에 저장하고 실행 즉시 사라졌겠지만, 클로저가 있음으로 인해서 해당 정보는 힙 영역에 보관되어서, 나중에 setTimeout 함수를 통해 log 함수가 호출되었을 때 참조될 수 있습니다. 그러나 var 키워드로 변수가 선언되었을 때는 클로저가 지역변수로써의 i 가 아닌 전역변수로써의 lexical environment 를 가지고 클로저를 구성하기 때문에 setTimeout 함수가 클로저를 참조하는 100 ms 시점에는 for 루프의 반복이 완전히 완료된 후의 값인 3인 상태를 참조하게 되기에 위와같은 결과가 나오게 됩니다.

이를 확인해 보기 위해서는 debugger를 붙여서 브라우저의 개발자 도구를 통해 단계별로 확인해 볼 수 있습니다.

for(var i = 0; i < 3; i++) {
	const log = () => {
      	debugger; // 디버거 기능
    	console.log(i);
    }
    setTimeout(log, 100);
}

면접에서 문제를 잘 해결하기 위해서는 천천히 코드를 살펴보면서 closure 를 중심으로 설명한다면, 올바른 결론에 도달하지 못하더라도 문제에서 요구하는 바를 충분히 잘 전달할 수 있을 것이라고 생각합니다.

profile
즐기면서 열심히!

0개의 댓글