Carrot
Front-end/JavaScript

[JavaScript] 클로저란?

NaDuck 2023. 6. 26. 11:53

자바스크립트는 함수 지향 언어다.

❌ 자바스크립트는 객체 지향 언어가 아니다.

  • 객체 지향 언어라고 할 순 없지만 class를 이용해 객체 지향 프로그래밍이 가능하다.
  • 다시 본론으로 돌아가, 자바스크립트는 함수 지향 언어로, 함수를 다른 함수의 인수로 넘길 수 있고, 외부의 함수를 호출할 수도 있다.
  • 이 때 함수를 사용하면서 여러 변수의 유효 범위가 정해진다.

 

중첩(nested) 함수

  • 자바스크립트에서 흔히 중첩 함수가 사용된다.
  • 중첩 함수는 함수 자체를 반환할 수 있고, 따라서 이렇게 반환된 함수는 어디서든 호출해 사용할 수 있다.
// makeCount 함수를 호출하여 다음 숫자를 반환해주는 counter 함수 생성 및 호출
function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2

👆 makeCounter() 함수를 호출하고 실행이 종료되었는데, 어떻게 내부 count 변수의 값을 참조할 수 있는 걸까? 이유는 렉시컬 환경 때문이다.

 

렉시컬 환경

  • 자바스크립트에서 실행 중인 함수, 코드 블록 { … }, 스크립트 전체는 렉시컬 환경이라 불리는 어떠한 객체를 갖는다.
  • 렉시컬 환경 객체는 크게 2가지로 구성된다.
    1. 환경 레코드(Environment Record): 모든 지역 변수를 프로퍼티로 저장하고 있는 객체. this 값도 여기에 저장되어 있다.
    2. 외부 렉시컬 환경에 대한 참조: 외부 코드와 연관되어 있는 렉시컬 환경을 참조할 수 있다.

대표적인 예시들은 다음과 같다.

1. 전역 렉시컬 환경

전역 렉시컬 환경

  • 위 그림에서 붉은 상자는 환경 레코드를 나타낸 것으로, 사실 “변수의 값을 가져오거나 변경하는 것”은 모두 “환경 레코드의 프로퍼티를 가져오거나 변경하는 것”을 의미한다.
  • 위 코드는 스크립트 최상단에 있으므로 전역 렉시컬 환경이라고 한다. 이 때 전역 렉시컬 환경은 외부 참조를 갖지 않기 때문에(전역 렉시컬 환경이 제일 상위이므로) 외부 렉시컬 환경은 null을 가리킨다.

 

2. 내부 & 외부 렉시컬 환경

  • 코드에서 변수에 접근할 때, 먼저 내부 렉시컬 환경에서 변수를 검색한다. 내부 렉시컬 환경에서 원하는 변수를 찾지 못하면 검색 범위를 내부 렉시컬 환경이 참조하는 외부 렉시컬 환경으로 확장한다. 마찬가지로 외부 렉시컬 환경에도 없다면 전역 렉시컬 환경으로 확장될 때까지 검색을 반복한다.

함수 내부의 렉시컬 환경

  1. 함수 say 내부의 alert 메소드에서 변수 name에 접근할 때, 먼저 내부 렉시컬 환경을 살펴본다. 내부 렉시컬 환경에서 변수 name: “John”을 찾았다.
  2. 마찬가지로 alert 메소드에서 변수 phrase에 접근하려는데 내부 렉시컬 환경에 없다. 따라서 외부 렉시컬 환경으로 검색 범위를 확장하고, 외부 렉시컬 환경에서 phrase: “hello”를 찾았다.

 

3. 함수를 반환하는 함수의 경우

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();
  • 다시 앞서 살펴봤던 중첩 함수의 예시 코드를 다시 보자. 다른 예시들과 달리 makeCounter 함수 안에 만들어지는 중첩 함수는, 생성되기만 하고 실행은 되지 않은 상태이다.
  • 여기서 중요한 점은, 모든 함수가 함수가 생성된 곳의 렉시컬 환경을 기억하고 있기 때문에 함수는 사실 [[Environment]]라 불리는 숨김 프로퍼티를 갖고 있고, 이를 통해 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장된다.

함수는 [[Environment]]라는 숨김 프로퍼티를 갖고 있다.

  • 따라서 counter.[[Environment]]{ count: 0 }이 있는 렉시컬 환경에 대한 참조가 저장된다. 호출 장소와 상관없이 함수 자신이 생성되는 곳을 기억할 수 있는 것은 사실 이 프로퍼티가 있어서 가능하고, 이 때 [[Environment]]는 함수가 생성될 때 딱 한 번 값이 세팅되고 영원히 변하지 않는다는 특징이 있다.

 

alert( counter() ); // 0
  • 이제 중첩 함수를 실제로 호출하면, 각 호출마다 새로운 렉시컬 환경이 생성되어 아래와 같은 렉시컬 환경을 갖는다. 그리고 새로 생성된 렉시컬 환경은 counter.[[Environment]]에 저장된 렉시컬 환경을 외부 렉시컬 환경으로서 참조하여 count 변수에 접근할 수 있는 것이다.

외부 렉시컬 환경을 참조하여 count 외부 변수에 접근하고 있다.

그리고 count++가 실행되면서 count 값이 1 증가하는데, 이 변수 값 갱신은 변수가 저장되어 있는 렉시컬 환경에서 이뤄진다. 즉 현재 시점에서 외부 렉시컬 환경에 있는 count 프로퍼티가 { count: 1 }로 수정되었다.

중첩 함수를 이용해 변수의 값을 변경한다.

 

클로저(clousure)

  • 클로저는 외부 변수를 기억하고 이 외부 변수에 접근할 수 있는 함수를 의미한다.
  • 자바스크립트에선 모든 함수가 자연스럽게 클로저가 된다. (new Function 제외)
// makeCount 함수를 호출하여 다음 숫자를 반환해주는 counter 함수 생성 및 호출
function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
  • 정리하자면 위 예시 코드에서 counter 함수 외부에 있는 count 변수에 접근할 수 있는 이유는 counter 함수가 생성될 당시 [[Environment]]로 counter 함수가 생성된 곳에 있는 렉시컬 환경을 기억하고 있기 때문이다. 그리고 이러한 counter 함수는 외부 변수인 count 변수에 접근할 수 있는 클로저이다.

 

클로저를 활용한 캡슐화

function Person(){
	var name = "";
        
	this.getName = function(){
		return name;
	};
        
  this.setName = function(n){
	  name = n;
	};
}
    
var person = new Person();
    
person.setName("kim");
console.log(person.getName());
  • 생성자 함수를 이용해 객체를 생성할 때, 클로저를 이용해 객체 프로퍼티의 캡슐화를 흉내낼 수 있다.
  • 위 예시 코드를 보면 각 getName, setName 메소드는 생성자 함수 Person의 스코프에 속한 name 변수를 기억하는 클로저이다. 생성자 함수 Person의 변수 name은 지역 변수이기 때문에 외부에서 접근할 수 없지만, name의 값을 기억하고 있는 getName, setName 메소드를 통해 접근이 가능하여 name 변수를 private하게 흉내낼 수 있다.

 

Reference

 

[javascript] 클로저(Closure)란?

클로저란? 함수 내부에 선언된 변수는 함수 종료 시, 모두 반환되지만, 외부에서 그 값을 계속 참조할 경우 반환하지 않습니다. 클로저는 이러한 특성을 이용해서 사용되는 데, 함수 내부의 변수

bamdule.tistory.com

 

변수의 유효범위와 클로저

 

ko.javascript.info