클로저(Closure)

·2024년 6월 5일
0

FE Interview

목록 보기
4/16

🔗 참고

  1. Github
  2. 자바스크립트 Deep Dive
  3. MDN : 클로저

📌 실행 컨텍스트

소스코드의 타입

코드설명
전역 코드전역에 존재하는 소스코드. 전역에 정의된 함수, 클래스 등의 내부코드는 포함되지 않는다.
함수 코드함수 내부에 존재하는 소스코드. 함수 내부에 중첩된 함수, 클래스 등의 내부 코드는 포함되지 않는다.
eval 코드빌트인 전역 함수인 eval 함수에 인수로 전달되어 실행되는 소스코드.
모듈 코드모듈 내부에 존재하는 소스코드. 모듈 내부의 함수, 클래스 등의 내부 코드는 포함되지 않는다.

소스코드의 평가와 실행

모든 소스코드는 실행에 앞서 평가 과정을 거친다. (자바스크립트 엔진은 소스코드를 '소스코드의 평가'와 '소스코드의 실행' 과정으로 나누어 처리한다.)

소스코드의 평가 과정에서는 실행 컨텍스트를 생성 → 변수, 함수 등의 선언문만 먼저 실행하여 생성된 변수나 함수 식별자를 키로 실행 컨텍스트가 관리하는 스코프(렉시컬 환경의 환경 레코드)에 등록한다.

소스코드의 평가 과정이 끝나면 비로소 선언문을 제외한 소스코드가 순차적으로 실행되기 시작한다.(=Hoisting) 이때 소스코드 실행에 필요한 정보(변수나 함수의 참조)를 실행 컨텍스트가 관리하는 스코프에서 검색하여 취득한다. 변수 값의 변경 등 소스코드의 실행 결과는 다시 실행 컨텍스트가 관리하는 스코프에 등록된다.

실행 컨텍스트는 소스코드를 실행하는데 필요한 환경을 제공하고 코드의 실행 결과를 실제로 관리하는 영역이다.
실행 컨텍스트는 식별자(변수, 함수, 클래스 등의 이름)를 등록하고 관리하는 스코프와 코드 실행 순서 관리를 구현한 내부 메커니즘으로, 모든 코드는 실행 컨텍스트를 통해 시리행되고 관리된다.

  • 식별자와 스코프를 실행 컨텍스트의 렉시컬 환경으로 관리
  • 코드 실행 순서는 실행 컨텍스트 스택으로 관리

렉시컬 환경

렉시컬 환경은 식별자와 식별자에 바인딩된 값, 그리고 상위 스코프에 대한 참조를 기록하는 자료구조. 실행 컨텍스트를 구성하는 컴포넌트이다.

렉시컬 환경은 키와 값을 갖는 객체 형태의 스코프를 생성 → 식별자로 키를 등록하고 식별자에 바인딩된 값을 관리한다.


📌 클로저

클로저는 자바스크립트 고유의 개념이 아닌 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다.

<MDN : 클로저>

  • 클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.
  • 클로저는 내부 함수에서 외부 함수의 범위에 대한 접근을 제공합니다.
  • JavaScript에서 클로저는 함수 생성 시 함수가 생성될 때마다 생성
const x = 1;

function outerFunc(){
  const x = 10;
  
  function innerFunc(){
    // innerFunc은 내부 함수이며 클로저.
    console.log(x); // 10
  }
  
  innerFunc();
}

outerFunc();

innerFunc는 outerFunc 내부에서 정의되고 호출된 중첩 함수이다. 중첩 함수 innerFunc의 상위 스코프는 외부함수 outerFunc의 스코프이다. → 중첩 함수 innerFunc 내부에서 outerFunc의 x 변수에 접근 가능

const x = 1;

function outerFunc(){
  const x= 10;
  innerFunc();
}

function innerFunc(){
  console.log(x); // 1
}

outerFunc();

innerFunc 함수가 outerFunc 함수 내부에서 정의된 중첩 함수가 아니므로 innerFunc 함수를 outerFunc 함수 내부에서 호출해도 outerFunc 함수의 변수에 접근이 불가능하다.

이런 현상이 발생하는 이유 : 자바스크립트가 렉시컬 스코프를 따르는 프로그래밍 언어이기 때문.


렉시컬 스코프

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

자바스크립트는 렉시컬 스코프를 따르므로 함수를 선언한 시점에 상위 스코프가 결정된다. 함수를 어디에서 호출하였는지는 스코프 결정에 아무런 의미를 주지 않는다.


클로저와 렉시컬 환경

function makeFunc() {
  const name = "Mozilla";
  function displayName() {
    console.log(name);
  }
  return displayName;
}

const myFunc = makeFunc();
myFunc();

위의 코드를 보면 makeFunc 함수는 중첩 함수 displayName을 반환하고 생명 주기를 마감하는 것으로 보인다. 그러나 실제 실행되었을 때 console.log(name)의 결과값은 Mozilla로 이미 생명 주기가 종료되어 실행 컨텍스트 스택에서 제거된 makeFunc 함수의 지역 변수 name이 다시 살아난 듯 보인다.

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

🔗 참고

위에서 클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이라고 하였는데, 이때 '그 함수가 선언된 렉시컬 환경'이란 함수가 정의된 위치의 스코프, 즉 상위 스코프를 의미하는 실행 컨텍스트의 렉시컬 환경을 의미한다.

실제로 makeFunc 함수의 실행이 종료되면 displayName 함수를 반환하면서 makeFunc 함수의 생명 주기가 종료된다. 즉, makeFunc 함수의 실행 컨텍스트가 실행 컨텍스트 스택에서 제거된다. 하지만 makeFunc 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거 되지만 makeFunc 함수의 렉시컬 환경까지 소멸하는 것이 아니다. 가비지 콜렉터는 누군가가 참조하고 있는 메모리의 공간을 함부로 해제하지 않기 때문이다.


클로저의 활용

클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다. 상태가 의도치 않게 변경되지 않도록 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.

예제 1

const increase = (function() {
  let num = 0; // 카운트 상태 변수
  
  // 클로저
  return function(){
    return ++num; // 카운트 상태를 1만큼 증가
  };
}());

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

예제 2

const counter = (function() {
  let num = 0;
  
  retrun {
    increase(){
      return ++num;
    },
    decrease(){
      return num > 0 ? --num : 0;
    }
  };
}());

console.log(counter.increase()) // 1
console.log(counter.increase()) // 2

console.log(counter.decrease()) // 1
console.log(counter.decrease()) // 0

예제 3

function makeCounter(aux){
  let counter = 0;
  
  return function(){
    counter = aux(counter);
    return counter;
  };
}

function increase(n){
  return ++n;
}

function decrease(n){
  return --n;
}

const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2

makeCounter 함수는 보조 함수를 인자로 전달받고 함수를 반환하는 고차 함수이다. makeCounter 함수가 반환하는 함수는 자신이 생성됐을 때 렉시컬 환경인 makeCounter 함수의 스코프에 속한 counter 변수를 기억하는 클로저이다.

makeCounter 함수를 호출해 함수를 반환할 때 반환된 함수는 자신만의 렉시컬 환경을 갖는다.

위 예제에서 전역 변수 increaser, decreaser에 할당된 함수는 각각 자신만의 독립된 렉시컬 환경을 갖기 때문에 카운트를 유지하기 위한 변수 counter를 공유하지 않아 카운터의 증감이 연동되지 않는다. 따라서 독립된 카운터가 아니라 연동하여 증감이 가능한 카운터를 만들려면 렉시컬 환경을 공유하는 클로저를 만들엉야 한다. 이를 위해서는 makeCounter 함수를 두 번 호출하지 말아야 한다.


정리

  • 클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 또한 함수가 속한 렉시컬스코프를 기억하여 함수가 렉시컬 스코프 밖에서 실행될 때도 그 스코프에 접근할 수 있게 하는 기능이다.
  • 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저라고 부른다.

0개의 댓글