JavaScript/JS

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

hihiha2 2023. 1. 26. 19:00

📌 클로저 사전지식

1️⃣ 실행컨텍스트

코드를 실행하는데 필요한 환경을 제공하는 객체

모든 자바스크립트의 모든것은 실행컨텍스트 내에서 발생한다.
이 실행컨텍스트는 전체 자바스크립트 코드가 실행되는 큰 상자 또는 컨테이너라고 가정할 수 있다.

자바스크립트를 실행시키면 자바스크립트 엔진은 콜스택이라는 통에 전역실행컨텍스트를 담는다.
만약 전역에서 함수A를 호출할 경우, 함수A의 실행 컨텍스트를 생성해서 또 스택에 담는다.

 

🔫 자바스크립트 실행 과정 🔫 

생성단계
실행컨택스트 생성
선언문만 실행해서 환경레코드에 기록

실행단계
선언문 외 나머지 코드 순차적 실행
환경레코드를 참조하거나 업데이트

 

1 함수를 호출한 후 순서대로 나열하면,
호출된 함수에 2 실행컨텍스트를 생성하고 이를 3 실행컨텍스트 스택에 push한다.
그 후 함수는 본인의 4 렉시컬 환경을 생성한다. 어떠한 코드가 어디에서 실행이 되고 본인주변에 어떤 코드들이 있는지 대체적인 정보를 담고 있는 환경이라고 할 수 있다. 렉시컬 환경은 함수 본인 내부의 식별자 그리고 식별자에 바인딩된 값 등을 기록하고 있는 하나의 자료구조이다.

 

마지막으로 코드의 실행이 끝나면 실행콘텍스트 스택에서 해당 컨텍스트를 pop하여 제거한다.

 

실행컨텍스트의 특징 중 클로저와 밀접한 관련이 있는 개념이 2가지가 있다.

  1. 렉시컬 환경의 생성
  2. 실행이 끝나면 스택에서 해당 컨텍스트를 제거하는 특징

2️⃣ 렉시컬 스코프

자바스크립트 엔진은 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다.
이를 렉시컬 스코프(정적스코프)라고 부른다.

자바스크립트는 렉시컬스코프를 따르기때문에 함수는 태어나자마자 상위 스코프가 결정이 되고 이후에 함수객체가 생성이 되면 해당함수객체는 본인의 상위 스코프를 항상 알 수 있게 된다. 해당 함수가 상위 스코프를 항상 알 수 있게 되는 이유는 자바스크립트는 태어나면서 자신의 내부 슬롯에 상위 스코프의 참조를 저장하기 때문이다.

 

3️⃣ 렉시컬환경

렉시컬환경은 환경레코드와 외부 렉시컬환경에 대한 참조로 이루어진다.

 

스코프에 포함된 식별자를 등록하고 등록된 식별자에 바인딩된 값을 관리하는 저장소

상위스코프를 가리킨다. 외부 렉시컬환경에 대한 참조를 통해 단방향 링크드 리스트인 스코프 체인을 구성한다.

 

1. 클로저의 정의 (MDN)

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
클로저는 함수와 그 외부를 둘러싸고 있는 렉시컬 환경의 조합이다.
In other words, a closure gives you access to an outer function's scope from an inner function.
다른 말로하면, 클로저란 내부함수에서 외부함수에 접근할 수 있는 권한을 주는 것을 말한다.
In JavaScript, closures are created every time a function is created, at function creation time.
자바스크립트에서 클로저는 함수가 생성되는 매순간 만들어진다.

➡️ 정의에서는 렉시컬환경과의 조합이라는 말이 중요한 것 같다.

일반적으로 함수가 실행되고 나면 생명주기가 끝이 나서 실행컨텍스트 내에 있는 call satck에서도 제거(pop)가 된다. 그러면 그 이후에 그 함수를 호출하면 아무 값도 나오지 않아야 할 것 같은데 이미 call stack에서 제거된 함수라고 하더라도 이 이후에 호출하면 값이 정상적으로 나온다. 이유가 뭘까? 그게 가능하도록 해주는 것이 바로 렉시컬환경이다.

자바스크립트의 모든 함수는 환경레코드와 자신의 상위 스코프를 기억한다. 따라서 함수가 이미 실행되어 스택에서 제거가 된 상태여도 렉시컬환경에는 남아있기때문에 호출을 해 값을 불러올수가 있는 것이다.

(Lexical) Closure. 클로저는 함수가 선언될 당시의 환경(environment)을 기억했다가 나중에 호출되었을때 원래의 환경에 따라 수행되는 함수이다. 이름이 클로저인 이유는 함수 선언 시의 scope(lexical scope)를 포섭(closure)하여 이후 실행될 때 이용하기 때문이다. 자주 '이름 없는 함수(익명함수)'와 혼동되곤 한다. 많은 언어의 익명함수가 closure를 포함하기 때문에 편하게 부를땐 서로 구분없이 부르기도 한다.

2. 구체적 예시

코드를 통해 살펴보자.

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

//outer함수를 호출하면 중첩함수 inner을 반환한다.
//그리고 outer함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const ella = outer(); //③
ella(); //④ 10

outer함수를 호출하면(③) outer함수는 중첩함수 inner을 반환하고 생명주기를 마감한다. (실행컨텍스트에서 제거). 이때 outer함수의 지역변수 x와 변수 값 10을 저장하고 있던 outer함수의 실행컨텍스트가 제거되었으므로 outer함수의 지역변수 x 또한 생명주기를 마감한다.
➡️ x변수에 접근할 수 있는 방법은 없어보인다.

But 위 코드의 실행결과(④)는 outer함수의 지역변수인 10이다. 이미 생명주기가 종료되어 실행컨텍스트 스택에서 제거된 outer함수의 지역변수가 다시 부활이라도 한듯이 동작하고있다

중첩함수 inner가 이미 생명주기를 마감한 outer함수 속 내부함수의 지역변수 x를 참조할 수 있다면 이때의 inner를 클로저라고 한다.

3. inner 함수가 클로저가 될 수 있었던 과정

outer함수는 실행컨텍스트에서 완전하게 제거가 되었다. 하지만 outer함수의 렉시컬환경까지 손을 대지는 않는다. ella는 inner함수 객체를 참조하고 있고 inner함수 객체는 본인의 내부슬롯에 저장된 outer함수의 렉시컬환경을 참조하기 때문에 없어지지 않는다.
따라서 ella에 의해 inner함수를 다시 호출하면 outer함수 안에 있는 10을 값으로 가지고 있는 변수x를 다시 참조할 수 있게 된다.
inner함수의 객체기준 내부함수는 생명주기를 마감하고 실행컨텍스트 스택에서 사라졌지만 기억하고 있는 내부슬롯에 저장된 상위스코프에 의존하여 상위스코프내의 식별자를 참조할 수 있게 되는것이다. 이것이 바로 클로저라는 개념이다.


 

✍️강의를 듣고 책을보면서 직접 공부하고 정리한 내용입니다📚
모던자바스크립트, mdn, 나무위키
우아한Tech 엘라의 Scope & Closure
하루의 실행 컨텍스트