Javascript 실행 컨텍스트와 클로저(2)

Benzy·2023년 8월 16일
0

JavaScript

목록 보기
5/5

클로저의 개념

그래서 클로저란 무엇인가?

클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical Environment)과의 조합이다.
- MDN

🤔 ...?

그래서 이게 뭔데...


개념 예제

function outerFunction() {
  var x = 10;
  var innerFunction = function() {
    console.log(x);
  }
  return innerFunction;
}

var inner = outerFunction();
inner(); // 10

실행 컨텍스트에 대해 공부했다면 이 코드를 보고 한 가지 의문이 들 수 있다.

마지막 줄 inner(), 즉, innerFunction을 호출할 때에는 outerFunction 함수는 실행 컨텍스트에서 제거된 후 일 것인데, 어떻게 outerFunction 내부의 변수인 x에 접근이 가능할까?


실행 컨텍스트를 이미지화 하면 이렇게 될 것이다.

함수 outerFunction을 호출하면 내부 함수 innerFunction이 반환되고, 함수 outerFunction의 실행 컨텍스트는 소멸한다. 자신을 포함하고 있는 외부 함수보다 내부 함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부 함수가 호출되더라도 외부함수의 지역변수에 접근할 수 있다. 이미 실행이 끝난 함수(실행 컨텍스트가 사라진)가 아직 살이있는 함수의 스코프 체인에 의해 변수객체가 유지되어 접근이 가능한 상황이라면, 아직 살아있는 함수(콜 스택에 실행 컨텍스트가 올라가 있는 함수)를 클로저(Closure)라고 부른다.

Lexical Scope

렉시컬 환경은 알지만 렉시컬 스코프는 뭘까?
렉시컬 환경과 렉시컬 스코프의 차이점에 대해서도 알아보았다.

1. 렉시컬 환경 (Lexical Environment)

렉시컬 환경은 실행 컨텍스트(Execution Context)에서 변수, 함수 선언 및 다른 식별자에 대한 정보를 담고 있는 데이터 구조이다. 각 실행 컨텍스트는 자체적으로 렉시컬 환경을 가진다.

2. 렉시컬 스코프 (Lexical Scope)

렉시컬 스코프는 변수 및 식별자의 유효 범위를 나타내는 개념이다. 함수가 어디에서 정의되었는지에 따라 해당 함수의 렉시컬 스코프가 결정된다. 이를 렉시컬 스코핑이라 한다. 렉시컬 스코프는 함수가 정의된 시점의 변수 및 스코프 상태를 기반으로 하며, 함수가 호출될 때마다 이 스코프에서 변수를 검색한다. 결국 렉시컬 스코프는 함수가 선언이 되는 위치에 따라서 상위 스코프가 결정되는 스코프이다.


function outerFunction() {
  
  let x = 'hello';
  
  function innerFunction() {
    console.log(x);
  }
  
  innerFunc();
}

outerFunction();

내부 innerFunction이 호출되면 자신의 실행 컨텍스트가 실행 컨텍스트 스택에 쌓이고 변수 객체(Variable Object)와 스코프 체인(Scope Chain) 그리고 this에 바인딩할 객체가 결정된다.
이때, 스코프 체인은 전역 객체와 함수 outerFunction의 스코프를 가리키는 함수 outerFunction의 활성 객체(변수 객체) 그리고 함수 자신의 스코프를 가리키는 활성 객체를 순차적으로 바인딩한다. 스코프 체인이 바인딩한 객체가 바로 렉시컬 스코프의 실체이다.

내부 함수 innerFunction가 자신을 포함하고 있는 외부함수 outerFunction의 변수 x에 접근할 수 있는 것, 다시 말해 상위 스코프에 접근할 수 있는 것은 렉시컬 스코프의 레퍼런스를 차례대로 저장하고 있는 실행 컨텍스트의 스코프 체인을 자바스크립트 엔진이 검색하였기에 가능한 것이다.

변수 객체와 스코프 체인이란?

실행 컨텍스트는 물리적으로 객체의 형태를 가지며, 3가지의 프로퍼티를 가지고 있다.

  1. 변수 객체 (Variable Object)
  2. 스코프 체인 (Scope Chain)
  3. this value

변수 객체 (VO)

실행 컨텍스트가 생성되면, 자바스크립트 엔진은 이 실행 컨텍스트에 필요한 정보를 담을 객체를 생성하는데 이를 변수 객체라고 한다.

실행 컨텍스트에 따라 가리키는 객체가 다르다.

  1. 전역 컨텍스트의 경우
  • 전역 객체 (Global Object / GO)를 가리킨다.
  • 모든 전역 변수와, 전역 함수를 프로퍼티로 갖는다.
  1. 함수 컨텍스트의 경우
  • 활성 객체(Activation Object / AO)를 가리킨다.
  • 지역 변수와 내부 함수를 프로퍼티로 갖는다.

스코프 체인 (Scope Chain)

  • 스코프 체인은 해당 컨텍스트가 참조할 수 있는 변수 객체(전역 객체 또는 활성 객체)의 레퍼런스를 담고 있는 리스트이다.
  • 먼저 현재 실행 컨텍스트의 활성 객체를 가리키고, 상위 컨텍스트의 활성 객체를 가리키다가, 마지막에는 전역 객체를 가리킨다.
  • 엔진은 스코프 체인을 통해 렉시컬 스코프를 파악한다.
  • 스코프 체인은 변수를 파악하기 위해서 현재 스코프, Activation Object에서 검색하고, 존재하지 않으면 스코프 체인에 담긴 순서대로 검색을 하게 된다.
  • 함수 컨텍스트의 스코프체인은 함수 호출 시 생성되며, Activation Object와 이 함수 내부 프로퍼티인 [[Scope]]로 구성되어 있다.

Scope = Activation Object + [[Scope]]

  • 함수 객체는 [[Scopes]] 프로퍼티를 갖고 있다.
    • [[Scopes]] 프로퍼티는 함수 객체의 내부 프로퍼티로 함수 객체가 실행되는 환경을 가리킨다.
      - 즉, 현재 실행 컨텍스트의 스코프 체인이 참조하고 있는 객체를 가리킨다.
    • [[Scopes]] 프로퍼티는 자신의 실행 환경 Lexical Environment와 자신을 포함하는 외부 함수의 실행 환경인 AO와 전역 객체(Global Object)를 가리키게 된다.
    • 이 때, 외부 함수의 실행 컨텍스트가 소멸하여도 [[Scopes]]는 외부 함수의 실행 환경을 여전히 참조할 수 있다. 이를 클로저라고 한다.

[[Scopes]] VS 스코프 체인

  • 함수의 생명 주기는 생성(Creation)과 호출(Call)로 구분된다.
  • [[Scopes]]는 함수 객체의 프로퍼티이며, 함수 생성단계에서 결정된다.
  • 스코프 체인은 함수 컨텍스트의 프로퍼티이고, 함수 호출 시 함수 컨텍스트가 생성 될 때, 제일 먼저 결정된다.

다시 처음의 정의로 돌아가보자.

클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical Environment)과의 조합이다.
-MDN-

위 정의에서 말하는 '함수'란 반환된 내부 함수를 의미하고, '그 함수가 선언될 때의 렉시컬 환경 (Lexical Environment)'이란 내부 함수가 선언됐을 때의 스코프를 의미한다. 즉, 클로저는 반환된 내부 함수가 자신이 선언됐을 때의 환경(Lexical Environment)인 스코프를 기억하며, 자신이 선언 됐을 때의 환경(Scope) 밖에서 호출되어도 그 환경(Scope)에 접근할 수 있는 함수를 말한다. 클로저는 자신이 생성될 때의 환경 (Lexical Environment)를 기억하는 함수다.

실행 컨텍스트의 관점에서 설명하면, 내부 함수가 유효한 상태에서 외부 함수가 종료하여 외부 함수의 실행 컨텍스트가 반환되어도 실행 컨텍스트 내의 활성 객체(Activation Object)는 내부 함수에 의해 참조되는 한 유효하여 내부 함수가 스코프 체인을 통해 참조할 수 있는 것을 의미한다.


클로저의 특징

  • 외부함수 스코프에서 내부함수 스코프로 접근 불가능하다.
  • 반대로 내부함수는 외부함수 스코프에 접근이 가능하다.
  • 외부 함수의 실행이 종료된 후에도, 클로저는 외부함수의 스코프 (Lexical Environment)에 접근할 수 있다.

예제

상태 유지

var box = document.querySelector('.box');
var toggleBtn = document.querySelector('.toggle');

var toggle = (function() {
  var isShow = false;
  
  return function() {
    box.style.display = isShow ? 'block' : 'none';
    isShow = !isShow
  }
})()

toggleBtn.onClick = toggle;
  • 즉시 실행 함수는 함수를 반환하고 즉시 소멸한다. 즉시 실행 함수가 반환한 함수는 자신이 생성 됐을 때의 렉시컬 환경(Lexical Environment)에 속한 변수 isShow를 기억하는 클로저다.
  • 클로저는 이벤트 핸들로 이벤트 프로퍼티에 할당됐다. 이벤트 프로퍼티에서 이벤트 핸들러인 클로저를 제거하지 않는 한, 클로저가 기억하는 렉시컬 환경의 변수 isShow는 소멸하지 않는다.
  • 버튼을 클릭하면 이벤트 프로퍼티에 할당된 이벤트 핸들리언 클로저 함수가 호출된다. 이때 .box 요소의 표시 상태를 나타내는 변수 isShow 값이 변경된다. 변수 isShow는 클로저에 의해 참조되고 있기 때문에 유효하며 자신의 변경된 최신 상태를 계속해서 유지한다.

이처럼 클로저는 현재 상태를 기억하고 상태가 변경되어도 최신 상태를 유지해야 하는 상황에 유용하다. 만약 클로저가 없다면 상태를 기억하고 변경하기 위해 전역 변수를 사용할 수 밖에 없다.

전역 변수의 사용 억제

var increaseBtn = document.getElementById('increase');
var count = document.getElementById('count');

var increase = (function() {
  var counter = 0;
  return function() {
    return ++counter;
  };
})();

increaseBtn.onClick = function() {
  count.innerHTML = increase();
};

함수형 프로그래밍에서 클로저를 활용한 예제

function makeCounter(predicate){
  var counter = 0;
  
  return function() {
    counter = predicate(counter);
    return counter;
  }
  
  function increase(n) {
    return ++n;
  }
  
  function decrease(n) {
    return --n;
  }
  
  
  // increaser 함수와 decreaser 함수는 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동되지 않는다.
  const increaser = makeCounter(increase);
  console.log(increaser()); // 1
  console.log(increaser()); // 2
  
  const decreaser = makecounter(decrease);
  console.log(decreaser()); // -1
  console.log(decreaser()); // -2
}

정보의 은닉

function Counter() {
  var counter = 0;
  
  this.increase = function() {
    return ++counter;
  };
  
  
  this.decrease = function(){
    return --counter;
  };
  
  const counter = new Counter();
  
  console.log(counter.increase()); // 1
  console.log(counter.decrease()); // 0
}
  • 생성자 함수 Counter는 increase, decrease 메소드를 갖는 인스턴스를 생성한다. 이 메소드들은 모두 자신이 생성 됐을 때의 렉시컬 환경인 생성자 함수 Counter의 스코프에 속한 변수 counter를 기억하는 클로저이며 렉시컬 환경을 공유한다. 생성자 함수가 생성한 객체의 메소드는 객체의 프로퍼티에만 접근할 수 있는 것이 아니며 기억하는 렉시컬 환경의 변수에도 접근할 수 있다.
  • 이때, 생성자 함수 Counter의 변수 counter는 this에 바인딩된 프로퍼티가 아니라 변수다. counter가 this에 바인딩된 프로퍼티라면 생성자 함수 Counter가 생성한 인스턴스를 통해 외부에서 접근이 가능한 public 프로퍼티가 되지만, 생성자 함수 Counter 내에서 선언한 변수 counter는 생성자 함수 Counter 외부에서 접근할 수 없다.
  • 하지만, 생성자 함수 Counter가 생성한 인스턴스의 메소드인 increase, decrease는 클로저이기 때문에 자신이 생성 됐을 때 렉시컬 환경인 생성자 함수 Counter의 변수 counter에 접근할 수 있다. 이러한 클로저의 특징을 사용해 클래스 기반 언어의 private 키워드를 흉내 낼 수 있다.

자주 발생하는 실수

var arr = [];

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

for (var i = 0; i < 5; i++) {
  arr[i] = (function(id) {
    return function() {
      return id; 
    };
  })(i); 
}

for (var j = 0; j < arr.length; j++) {
  console.log(arr[j])());
}
  1. 배열 arr에는 즉시 실행 함수에 의해 함수가 반환된다.
  2. 이때 즉시 실행 함수는 i를 인자로 전달 받고 매개변수 id에 할당한 후 내부 함수를 반환하고 life-cycle이 종료된다. 매개변수 id는 자유변수가 된다.
  3. 배열 arr에 할당된 함수는 id를 반환한다. 이때 id는 상위 스코프의 자유변수이므로 그 값이 유지된다.

위 예제는 자바스크립트 함수 레벨 스코프의 특성으로 인해 for 루프의 초기문에서 사용된 변수의 스코프가 전역이 되기 때문에 발생하는 현상이다. ES6의 let 키워드를 사용하면 해결할 수 있다.

const arr = [];

for (let i = 0; i < 5; i++) {
  arr[i] = function() {
    return i;
  };
};
 
for(let i = 0; i < arr.length; i++) {
	console.log(arr[i]());
};

고차 함수를 이용한 방법

const arr = new Array(5).fill();

arr.forEach((value, index, array) => array[index] = () => index);
arr.forEach(f => console.log(f()));

Reference

https://poiemaweb.com/js-closure
https://blacklobster.tistory.com/7
https://yoo11052.tistory.com/154
https://velog.io/@yejineee/실행-컨텍스트의-세-가지-객체-VO-SC-this

profile
상호작용을 구현하는 개발자

1개의 댓글

comment-user-thumbnail
2023년 8월 16일

개발자로서 배울 점이 많은 글이었습니다. 감사합니다.

답글 달기