본문 바로가기

[자바스크립트] 클로저 (feat 코어 자바스크립트)

 

 

들어가며

많은 기업들이 타입 스크립트와 nest.js를 활용해서 서버 개발을 하곤 합니다. 취업을 하려면, 타입 스크립트와 nest.js를 공부해서 실무를 익히는 것이 중요할 것입니다. 하지만 아직 자바스크립트의 기초도 없는 상태에서 타입 스크립트와 nest.js를 공부하는 것이 맞을까 하는 생각이 들었습니다. 빠르게 기술변화를 적응하고, 러닝 커브를 줄이기 위해 빠르게 공부해야 하는 것도 맞겠지만, 그전에 언어의 기반이 되는 자바스크립트부터 제대로 알아야 하지 않을까 하는 생각이 들었습니다. 이 기회에 자바스크립트의 기본에 대해 정리해보고자 합니다. 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

클로저의 의미 및 원리 이해

앞에서 실행컨텍스트에 대해 배운 지식을 바탕으로 클로저를 정의해본다면, 클로저란 외부 함수의 변수를 참조하는 내부 함수를 외부로 전달할 때 외부 함수의 실행 컨텍스트가 종료된 후에도 외부 함수를 참조할 수 있는 현상이라고 생각합니다. 제가 왜 클로저를 정의했는지 예제를 통해 살펴보겠습니다. 우선 외부 함수에서 변수를 선언하고 내부 함수에서 해당 변수를 참조하는 형태의 간단한 코드를 작성해보겠습니다. 

 

 

var outer = function () {
  var a = 1;
  var inner = function () {
    console.log(++a);
  };
  inner();
};
outer();

 

outer 함수에서 변수 a를 선언했고, outer의 내부 함수인 inner 함수에서 a의 값을 1만큼 증가시킨 다음 출력합니다. inner 함수 내부에서는 a를 선언하지 않았기 때문에 environmentRecord에서 값을 찾지 못하므로 outerEnvironmentReference에 지정된 상위 컨텍스트인 outer의 LexicalEnvironment에 접근해서 다시 a를 찾습니다. 4번째 줄에서는 2가 출력됩니다. outer 함수의 실행 컨텍스트가 종료되면 LexicalEnvironment에 저장된 식별자(a, inner)에 대한 참조를 지웁니다. 그러면 각 주소에 저장돼 있던 값들은 자신을 참조하는 변수가 하나도 없게 되므로 가비지 컬렉터의 수집 대상이 될 것입니다. 

 

 

다른 예제도 살펴보겠습니다.

 

 

var outer = function () {
  var a = 1;
  var inner = function () {
    return ++a;
  };
  return inner();
};
var outer2 = outer();
console.log(outer2);

 

 

이번에도 inner 함수 내부에서 외부 변수인 a를 사용했습니다. 그런데 6번째 줄에서는 Inner 함수를 실행한 결과를 리턴하고 있으므로 결과적으로 outer 함수의 실행 컨텍스트가 종료된 시점에는 a 변수를 참조하는 대상이 없어집니다. 위의 예제와 마찬가지로 a, inner 변수의 값들은 언젠가 가비지 컬렉터에 의해 소멸할 것입니다. 역시 일반적인 함수 및 내부 함수에서의 동작과 차이가 없습니다. 

 

위 두 outer 함수의 실행 컨텍스트가 종료되기 이전에 inner 함수의 실행 컨텍스트가 종료돼 있으며, 이후 별도로 inner 함수를 호출할 수 없다는 공통점이 있습니다. 그렇다면 outer의 실행 컨텍스트가 종료된 후에도 inner 함수를 호출할 수 있게 만들면 어떨까요?  

 

 

var outer = function () {
  var a = 1;
  var inner = function () {
    return ++a;
  };
  return inner;
};
var outer2 = outer();
console.log(outer2());
console.log(outer2());

 

 

이번에는 6번째 줄에서 inner 함수의 실행 결과가 아닌 inner 함수 자체를 반환했습니다. 그러면 outer 함수의 실행 컨텍스트가 종료될 때 outer2 변수는 outer의 실행 결과인 inner 함수를 참조하게 될 것입니다. 이후 9번째에서 outer2를 호출하면 앞서 반환된 함수인 inner가 실행될 것입니다. 

 

inner 함수의 실행 컨텍스트의 environmentRecord에는 수집할 정보가 없습니다. outer-EnvironmentReference에는 inner 함수가 선언된 위치의 LexicalEnvironment가 참조 복사됩니다. inner 함수는 outer 함수 내부에서 선언됐으므로, outer 함수의 LexicalEnvironment가 담길 것입니다. 이제 스코프 체이닝에 따라 outer에서 선언한 변수 a에 접근해서 1만큼 증가시킨 후 그 값인 2를 반환하고, inner 함수의 실행 컨텍스트가 종료됩니다. 10번째 줄에서 다시 outer2를 호출하면 같은 방식으로 a의 값을 2에서 3으로 1 증가시킨 후 3을 반환합니다. 

 

그런데 이상한 점이 있습니다. Inner 함수의 실행 시점에는 outer 함수는 이미 실행이 종료된 상태인데 outer 함수의 LexicalEnvironment에 어떻게 접근할 수 있는 것일까요? 이는 가비지 컬렉터의 동작 방식 때문입니다. 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않습니다. 위의 예시에서 outer 함수는 실행 종료 시점에 inner 함수를 반환합니다. 외부 함수인 outer의 실행이 종료되더라도 내부 함수인 inner 함수는 언젠가 outer2를 실행함으로써 호출될 가능성이 열린 것입니다. 언젠가 inner 함수의 실행 컨텍스트가 활성화되면 outerEnvironmentReference가 outer 함수의 LexicalEnvironment를 필요로 할 것이므로 수집 대상에서 제외됩니다. 그 덕에 inner 함수가 이 변수에 접근할 수 있는 것입니다. 

 

이를 통해 다시 정리해보면, 클로저란 외부 함수의 변수를 참조하는 내부 함수를 외부로 전달할 때 외부 함수의 실행 컨텍스트가 종료된 후에도 외부 함수를 참조할 수 있는 현상이라고 말할 수 있다고 생각합니다. 그렇다면, 클로저를 활용할 때 메모리 관리는 어떻게 해야 할까요? 이에 대해 알아보겠습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

클로저와 메모리 관리

클로저는 내부 함수가 외부 함수의 값을 참조해야 할 때, 의도적으로 함수의 지역변수 메모리를 소모하도록 함으로써 발생합니다. 그렇다면 효율적인 메모리 관리를 하려면 어떻게 해야 할까요? 해결책은 지역변수의 필요성이 사라진 시점에 더는 메모리를 소모하지 않게 해 주면 됩니다. 참조 카운트를 0으로 만들면 언젠가 GC가 수거해갈 것이고, 이때 소모됐던 메모리가 회수될 것입니다. 참조 카운트를 0으로 만드는 방법은? 식별자에 참조형이 아닌 기본형 데이터(보통 null이나 undefined)를 할당하면 됩니다. 다음은 코드에 메모리 해제 코드를 추가한 코드입니다.

 

// (1) return에 의한 클로저의 메모리 해제
var outer = (function () {
	var a = 1;
    var inner = function() {
    	return ++a;
    };
	return inner;
})();
console.log(outer());
console.log(outer());
outer = null; // outer 식별자의 inner 함수 참조를 끊음

 

 

그렇다면, 클로저는 어떤 상황에서 사용할 수 있을까요? 이에 대해 알아보겠습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

클로저 활용 사례

본격적으로 실제로 어떤 상황에서 클로저가 등장하는지 살펴보겠습니다.

 

 

1. 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때

대표적인 콜백 함수 중 하나인 이벤트 리스너에 관하여 알아보겠습니다. 

 

var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');

fruits.forEach(function(fruit) {
	var $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', function(){
    	alert('your choice is ' + fruit);
    });
    $ul.appendChild($li);
});
document.body.appendChild($ul);

 

fruits 변수를 순회하며 li를 생성하고, 각 li를 클릭하면 해당 리스너에 기억된 콜백 함수를 실행하게 했습니다. 4번째 줄의 forEach 메서드에 넘겨준 익명의 콜백 함수(A)는 그 내부에서 외부 변수를 사용하지 않고 있으므로 클로저가 없지만, 7번째 줄의 addEventListener에 넘겨준 콜백 함수(B)에는 fruit이라는 외부 변수를 참조하고 있으므로 클로저가 있습니다. (A)는 fruits의 개수만큼 실행되며, 그때마다 새로운 실행 컨텍스트가 활성화될 것입니다. A의 실행 종료 여부와 무관하게 클릭 이벤트에 의해 각 컨텍스트의 (B)가 실행될 때는 (B)의 outerEnvironmentReference가 (A)의 LexicalEnvironment를 참조하게 됩니다. 따라서 최소한 (B) 함수가 참조할 예정인 변수 fruit에 대해서는 (A)가 종료된 후에도 GC 대상에서 제외되어 계속 참조 가능할 것입니다.

 

그런데 (B) 함수가 콜백 함수뿐 아니라 다른 곳에서도 쓰인다면, 반복을 줄이기 위해 (B)를 외부로 분리하는 편이 나을 수 있을 것입니다. B함수를 외부에서 사용하는 방법을 사용해보겠습니다.

 

 

var alertFruit = function(fruit){
	alert('your choice is ' + fruit);
};

fruits.forEach(function(fruit) {
	var $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', alertFruit);
    $ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);

 

위의 예시에서는 공통 함수로 쓰고자 콜백 함수를 외부로 꺼내어 alertFruit라는 변수에 담았습니다. 이제 alertFruit을 직접 실행할 수 있습니다. 또한 14번째 줄에서는 정상적으로 'banana'에 대한 얼럿이 실행됩니다.

 

 

 

 

 

 

그런데 각 li를 클릭하면 클릭한 대상의 과일명이 아닌 [object MouseEvent]라는 값이 출력됩니다. 콜백 함수의 인자에 대한 제어권을 addEventListener가 가진 상태이며, addEventListener는 콜백 함수를 호출할 때 첫 번째 인자에 '이벤트 객체'를 주입하기 때문입니다. 이 문제는 bind 메서드를 활용하면 손쉽게 해결할 수 있습니다. 

 

 

var alertFruit = function(fruit){
	alert('your choice is ' + fruit);
};

fruits.forEach(function(fruit) {
	var $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', alertFruit.bind(null, fruit));
    $ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);

 

다만 이렇게 하면 이벤트 객체가 인자로 넘어오는 순서가 바뀌는 점 및 함수 내부에서의 this가 원래의 그것과 달라지는 점은 감안해야 합니다. 이런 변경사항이 발생하지 않게끔 하면서 이슈를 해결하기 위해서는 bind 메서드가 아닌 다른 방식으로 풀어내야만 합니다. 여기서 다른 방식이란 고차 함수를 활용하는 것으로, 함수형 프로그래밍에서 자주 쓰이는 방식이기도 합니다. 

 

 

var alertFruitBuilder = function (fruit){
	return function() {
    	alert('your choice is ' + fruit);
    }
}

fruits.forEach(function (fruit) {
	var $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', alertFruitBuilder(fruit));
    $ul.appendChild($li);
});

 

4번째 줄에서 alertFruit 함수 대신 alertFruitBuilder라는 이름의 함수를 작성했습니다. 이 함수 내부에서는 다시 익명 함수를 반환하는데, 이 익명 함수가 바로 기존의 alertFruit 함수입니다. 12번째 줄에서는 alertFruitBuilder 함수를 실행하면서 fruit 값을 인자로 전달했습니다. 그러면 이 함수의 실행 결과가 다시 함수가 되며, 이렇게 반환된 함수를 리스너에 콜백 함수로써 전달할 것입니다. 이후 언젠가 클릭 이벤트가 발생하면 비로소 이 함수의 실행 컨텍스트가 열리면서 alertFruitBuilder의 인자로 넘어온 fruit를 outerEnvironmentReference에 의해 참조할 수 있습니다. 즉, alertFruitBuilder의 실행 결과로 반환된 함수에는 클로저가 존재합니다.

 

지금까지 콜백 함수 내부에서 외부 변수를 참조하기 위한 방법 세 가지를 살펴봤습니다.

1. 콜백 함수를 내부 함수로 선언해서 외부 변수를 직접 참조하는 방법으로, 클로저를 사용한 방법. 

2. bind 메서드로 값을 직접 넘겨준 덕분에 클로저는 발생하지 않게 된 반면 여러 가지 제약사항이 따르게 됐습니다. 

3. 콜백 함수를 고차 함수로 바꿔서 클로저를 적극적으로 활용한 방법

 

이 세 방법의 장단점을 각기 파악하고 구체적인 상황에 따라 어떤 방법을 도입하는 것이 가장 효과적일지 고민해야 합니다. 

 

 

 

 

 

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

두 번째 정보 은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈 간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념 중 하나입니다. 흔히 접근 권한에는 public, private, protected의 세 종류가 있습니다. 각 단어의 의미 그대로, public은 외부에서 접근 가능한 것이고, private은 내부에서만 사용하며 외부에 노출되지 않는 것을 의미합니다. 

 

자바스크립트는 기본적으로 변수 자체에 이러한 접근 권한을 직접 부여하도록 설계돼 있지 않습니다. 그렇다고 접근 권한 제어가 불가능한 것은 아닙니다. 클로저를 이용하면 함수 차원에서 public 한 값과 private 한 값을 구분하는 것이 가능합니다. 다음 예제를 살펴보겠습니다.

 

 

var outer = function () {
	var a = 1;
    var inner = function() {
    	return ++a;
    }
	return inner;
};

var outer2 = outer();
console.log(outer2());
console.log(outer2());

 

outer 함수를 종료할 때 inner 함수를 반환함으로써 outer 함수의 지역변수인 a의 값을 외부에서도 읽을 수 있게 됐습니다. 이처럼 클로저를 활용하면 외부 스코프에서 함수 내부의 변수들 중 선택적으로 일부의 변수에 대한 접근 권한을 부여할 수 있습니다. 바로 return을 활용해서 말입니다. 

 

closure라는 영어 단어는 사전적으로 '닫혀있음', 폐쇄성, 완결성' 정도의 의미를 가집니다. 이 폐쇄성에 주목해보면 위 예제를 조금 다르게 받아들일 수 있습니다. outer 함수는 외부(전역 스코프)로부터 철저하게 격리된 닫힌 공간입니다. 외부에서는 외부 공간에 노출돼 있는 outer라는 변수를 통해 outer 함수를 실행할 수는 있지만, outer 함수 내부에는 어떠한 개입도 할 수 없습니다. 외부에서는 오직 outer 함수가 return 한 정보에만 접근할 수 있습니다. return 값이 외부에 정보를 제공하는 유일한 수단입니다.

 

그러니까 외부에 제공하고자 하는 정보들을 모아서 return 하고, 내부에서만 사용할 정보들은 return 하지 않는 것으로 접근 권한 제어가 가능합니다. return 한 변수들은 공개 멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 됩니다. 

 

지금까지 클로저의 개념을 활용해서 외부 스코프에서 함수 내부의 변수 중 일부에 접근하는 방법에 대해 익혀봤는데, 이번에는 클로저를 통해 접근하는 것이 아닌, getter와 setter 개념을 통해 접근하는 방법에 대해 알아보겠습니다. 

 

 

 

 

 

 

 

1. 접근자 프로퍼티

접근자란 객체 지향 프로그래밍에서 객체가 가진 프로퍼티 값을 객체 바깥에서 읽거나 쓸 수 있도록 제공하는 메서드를 말합니다. 그리고 접근자 프로퍼티란 값을 획득(get)하고 설정(set)하는 역할을 담당합니다. 외부 코드에서는 함수가 아닌 일반적인 프로퍼티처럼 보입니다.

 

 

1.1 getter와 setter

접근자 프로퍼티는 'getter(획득자)'와 'setter(설정자)' 메서드로 표현됩니다. 객체 리터럴 안에서 getter와 setter 메서드는 get과 set으로 나타낼 수 있습니다. 

 

let obj = {
 get propName() {
   // getter, obj.propName을 실행할 때 실행되는 코드
 },

 set propName(value) {
   // setter, obj.propNAme = value를 실행할 때 실행되는 코드
 }
};

 

getter 메서드는 obj.propName을 사용해 프로퍼티를 읽으려고 할 때 실행됩니다. setter 메서드는 obj.propName = value으로 프로퍼티에 값을 할당하려 할 때 실행됩니다. 예를 통해 getter와 setter에 대해 살펴보겠습니다.

 

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

alert(user.fullName); // John Smith

 

바깥 코드에선 접근자 프로퍼티를 일반 프로퍼티처럼 사용할 수 있습니다. 접근자 프로퍼티는 이런 아이디어에서 출발했습니다. 접근자 프로퍼티를 사용하면 함수처럼 호출하지 않고, 일반 프로퍼티에서 값에 접근하는 것처럼 평범하게 user.fullName을 사용해 프로퍼티 값을 얻을 수 있습니다. 나머지 작업은 getter 메서드가 뒷단에서 처리해줍니다. 한편 위 예시의 fullName은 getter 메서드만 가지고 있기 때문에 user.fullName = 을 사용해 값을 사용해 값을 할당하려고 하면 에러가 발생합니다.

 

 

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

user.fullName = "Test"; // Error (프로퍼티에 getter 메서드만 있어서 에러가 발생합니다.)

 

하지만 setter메서드가 있다면, 이 문제를 해결할 수 있습니다. 

 

 

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  },
  
  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  }
};

// 주어진 값을 사용해 set fullName이 실행됩니다.
user.fullName = "Alice Special"

alert(user.fullName); // Alice Special
alert(user.name); // Alice
alert(user.surname); // Special

 

이렇게 getter와 setter 메서드를 구현하면 객체엔 fullName이라는 '가상'의 프로퍼티가 생깁니다. 가상의 프로퍼티는 읽고 쓸 순 있지만 실제로는 존재하지 않습니다. 그럼 getter와 setter의 사용에 대해 조금 더 자세하게 알아보겠습니다.

 

 

 

 

 

1.2 getter와 setter 똑똑하게 활용하기

일반적으로 이름을 읽고 수정하는 객체는 다음과 같이 이름을 수정하는 메서드 setName()을 포함하고 있습니다.

 

let user = {
  name: '',
  setName(value) {
    if (value.length < 4) {
      alert("입력하신 값이 너무 짧습니다. 네 글자 이상으로 구성된 이름을 입력하세요.");
      return;
    }
    this.name = value;
  }
};

user.setName("Pete");
alert(user.name); // Pete

user.setName(""); // 너무 짧은 이름을 할당하려 함

 

그러나 getter와 setter를 '실제' 프로퍼티 값을 감싸는 래퍼(wrapper)처럼 사용하면, 메서드를 새로 만드는 일 없이 프로퍼티 값을 원하는 대로 통제할 수 있습니다.

 

let user = {
  get name() {
    return this._name;
  },

  set name(value) {
    if (value.length < 4) {
      alert("입력하신 값이 너무 짧습니다. 네 글자 이상으로 구성된 이름을 입력하세요.");
      return;
    }
    this._name = value;
  }
};

user.name = "Pete";
alert(user.name); // Pete

user.name = ""; // 너무 짧은 이름을 할당하려 함

 

user의 이름은 _name에 저장되고, 프로퍼티에 접근하는 것은 getter(user.name)와 setter(user.name = value)를 통해 이뤄집니다. 기술적으론 외부 코드에서 user._name을 사용해 이름에 바로 접근할 수 있습니다. 그러나 밑줄(user._name)로 시작하는 프로퍼티는 객체 내부에서만 활용하고, 외부에서는 건드리지 않는 것이 관습입니다. 위의 예제에서 user.name을 통해 프로퍼티에 접근하고 수정하는 것처럼 user._name을 직접적으로 사용하지는 않는 것이 좋습니다. 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

클로저 정리

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

 

내부 함수를 외부로 전달하는 방법에는 함수를 return 하는 경우뿐 아니라 콜백으로 전달하는 경우도 포함됩니다. 

 

클로저는 그 본질이 메모리를 계속 차지하는 개념이므로 더는 사용하지 않게 된 클로저에 대해서는 메모리를 차지하지 않도록 관리해줄 필요가 있습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

마치며

자바스크립트의 기본에 대해 공부하면서, 기본도 정확하게 알지 못하고, 앞으로 나아가려 했구나 하는 생각이 들었습니다. 기초이기에 지금 공부하는 내용을 더 잘 이해해야겠다고 생각했습니다. 기본에 충실할 수 있는 개발자가 되고 싶습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

출처

 

싸니까 믿으니까 인터파크도서

 

book.interpark.com

 

클로저 - JavaScript | MDN

클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.

developer.mozilla.org

 

JavaScript - 접근자 프로퍼티 (getter, setter)

접근자 프로퍼티(accessor property) : 값이 없음. 프로퍼티를 읽거나 쓸 때 호출하는 함수를 값 대신에 지정할 수 있는 프로퍼티입니다. 접근자 프로퍼티의 본질은 함수인데, 이 함수는 값을 획득(get)

velog.io