본문 바로가기

Javascript

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

들어가며

클로저는 자바스크립트의 주요 특징으로 뽑히는 개념이지만, 그만큼 애매하고 헷갈리는 개념이기도 하다. 

검색하면 수십 수백가지의 자료가 나오긴 하지만, 열 번 읽는 것보다 한 번 정리해서 써보는게 더 빠르게 내 것으로 만들 수 있다고 생각하기에 내 언어로 다시 정리해보는 글.

 

MDN에 클로저를 검색하면 나오는 문장은 아래와 같다.

 

클로저는 함수와 함수가 선언된 어휘적 환경(Lexical Environment)의 조합이다.

 

그래서 다시..말하자면..

 

이 문장을 통해 클로저를 알기 위해선 Lexical Environment에 대한 이해가 필요하다는 것을 알 수 있다.

Lexical Environment의 토대가 되는 실행 컨텍스트부터 차근차근 알아가며 클로저와 가까워보자.

실행 컨텍스트

실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로, 활성화 될 때 호이스팅(선언된 변수를 위로 끌어올림)이 일어나고 외부 환경 정보를 구성하고, this값을 수정하는 등의 동작을 수행한다. 

 

자바스크립트 코드가 실행되는 순간 전역 컨텍스트가 콜 스택에 담기고, 이후 호출되는 함수의 컨텍스트가 콜 스택의 상단에 담는다.

자바스크립트는 콜 스택의 가장 최상단에 위치한 실행 컨텍스트의 코드를 순차적으로 실행한다. 

 

outer함수와 inner함수를 통해 카운터를 구현하는 간단한 예시를 통해 코드로 알아보자.

 

// (1) 자바스크립트 코드 실행 => 전역 컨텍스트 생성
const outer = () => {
  let a = 0;
  const inner = () => {
    return ++a 
  }
  inner(); // (3) inner 함수 호출 => inner 실행 컨텍스트 생성
};

outer(); // (2) outer 함수 호출 => outer 실행 컨텍스트 생성

// inner 컨텍스트 제거 => outer 컨텍스트 제거 => 전역 컨텍스트 제거

 

위 코드의 흐름을 도식화하면 아래와 같다.

전역 컨텍스트와 관련된 코드를 순차로 실행하다가 어떤 함수가 호출되면 그 함수에 대한 환경 정보를 수집해 실행 컨텍스트를 생성하고, 콜 스택에 담아 해당 함수의 코드를 순차적으로 실행한다. 이 과정을 모든 실행 컨텍스트가 종료될 때까지 반복한다. 

 

이 중 환경 정보를 수집할 때 아래의 세 정보를 수집한다.

 

(1) VariableEnvironment

- 실행 컨텍스트가 생성될 때 컨텍스트 안의 식별자, 외부 환경 정보의 캡쳐본

- 식별자 정보들을 저장하는 environmentRecord와, 바로 직전 컨텍스트의 LexicalEnvironment 정보를 참조하는 outer-environmentRecord로 이루어져있다.

- 변경이 되어도 반영되지 않는다. 

 

(2) LexicalEnvironment

- variable environment의 복사본으로, 변경사항이 반영된다.

 

(3) ThisBinding

- this : 식별자가 바라봐야할 대상 객체

- 실행 컨텍스트를 활성화할 때 지정된 this가 저장됨 (지정되지 않으면 전역 객체가 저장)

클로저(Closure)란?

클로저는 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우, A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상을 말한다.

 

이 때 외부로 전달한다는 것은 외부함수의 결과값으로 내부함수를 return해서 전달하는 것뿐만 아니라, setTimeout같은 window 메서드의 콜백함수나 addEventListener에 등록하는 handler 함수의 경우도 포함한다.

 

아래 예시를 통해 코드로도 살펴보자.

 

const outer = () => {
  let a = 1;
  const inner = () => {
    return ++a //outer함수의 변수 a 참조
  }
  inner(); 
};

const outer2 = outer(); //outer함수의 실행 컨텍스트 종료
console.log(outer2()); //inner함수 실행, 결과값: 2
console.log(outer2()); //inner함수 실행, 결과값: 3

 

outer2가 선언될 때를 보면 outer2는 outer함수의 결과값인 inner함수를 참조하게 되고, 이 때 outer함수의 실행 컨텍스트는 종료된다. 

하지만 outer2가 실행될 때를 보니 실행 컨텍스트가 종료된 outer함수의 변수 a를 활용해 결과값이 계속 나오고 있다.

이렇게 외부 함수의 LexicalEnvironment가 가비지 컬렉팅되지 않고 살아남는 현상을 클로저라 한다.

클로저(Closure)의 특징과 활용

클로저의 특징을 활용한 실용적인 예제 코드를 알아보자.

1. 캡슐화

MDN에 의하면 클로저는 데이터(lexical environment)와 그 데이터를 조작하는 함수를 연관시켜주므로, 객체지향 프로그래밍과 같은 맥락에 있다. 따라서 하나의 메소드를 가진 객체를 사용하는 모든 곳에 클로저를 사용할 수 있다.

 

간단한 예시로는 p태그를 클릭할 때 폰트 컬러가 변경되는 함수를 생각해볼 수 있다.

 

  //색상 변경 함수를 리턴하는 함수
  function setColor (color) {
     return function() { 
       document.body.style.fontColor = color;
     };
   }

  const colorRed = setColor(red);
  const colorBlue = setColor(blue);
  const colorYellow = setColor(yellow);

  //p태그의 onClick 이벤트에 전달
  document.getElementById('color-red').onclick = colorRed;
  document.getElementById('color-blue').onclick = colorBlue;
  document.getElementById('color-yellow').onclick = colorYellow;

  <p id="color-red">red</p>
  <p id="color-blue">blue</p>
  <p id="color-yellow">yellow</p>

 

2. 정보 은닉 (접근 권한 제어)

function tellMyLover(name) {
    const _name = name;
    return function () {
        console.log(`My lover is ${_name}`);
    };
}

const tellSecret = tellMyLover('YOU');

tellSecret(); //My lover is YOU

 

my lover를 변경할 수 없는, 아주 느끼한 예제 코드를 통해 알 수 있는 사실 

클로저는 변수의 접근 권한을 제어할 수 있다.

마무리하며

워낙 자바스크립트의 중요 개념 중 하나인지라 파면 팔수록 내용이 많지만, 한 포스팅에 모든 걸 담겠다는 의지하엔 작성 완료까지 너무 오랜 시간이 걸릴 것 같아 확실히 정리한 부분까지 적어보았다. 리액트의 useState가 클로저를 활용한 개념이라는 이야기를 들은 적이 있는데, 매일 useState를 활용하고 있는 자로서 그 부분도 차차 알아가보아야겠다는 생각이 든다.

 

참고 학습자료
코어자바스크립트 - 정재남
클로저 - MDN