본문 바로가기
JavaScript

클로저의 활용

by oncerun 2022. 2. 24.
반응형

 

지금까지 클로저의 동작원리에 대해서 알아보았다.  상위 스코프의 식별자를 외부 함수의 생명 주기 소멸 이후에도 참조한다는 개념을 어디다 사용해야 효과적으로 클로저를 활용했다고 하는 것일까?

 

클로저는 상태를 안전하게 변경하고 유지하기 위해서 사용한다.  

즉 상태를 외부로부터 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.  

 

아. 캡슐화? 전에서 클로저는 함수가 자유 변수에 대해 닫혀있다는 의미라고 했다. 자유 변수 즉 상태에 대해 묶여있는 함수이구나라고 생각된다. 

 

요청사항을 하나 만들어보자.   요청은 함수 호출 시 호출된 횟수를 누적하여 출력하는 것이다. 

let num = 0;

const increase = function(){
	return ++num;
}

increase();

 

이 코드는 오류의 가능성을 품고 있는 코드이다. 원하는 대로 동작하기 위해선 전제 조건이 붙기 때문이다.

  • num 변수는 increase 함수가 호출되기 전까지는 변경되지 않고 유지되어야 한다.
  • 이를 위해 increase 함수만이 num 변수를 변경할 수 있어야 한다.

 

하지만 num은 전역 변수를 통해 관리되어 누구나 접근하고 그 값을 변경할 수 있다. 

따라서 상태를 안전하게 감추기 위해 increase 함수 내부에 지역 변수로 선언해야 할까? 이도 호출마다 값을 초기화시키기 때문에 불가능하다.  

 

우리는 상태를 안전하게 관리하기 위해 클로저를 사용할 수 있다.

const increase = (function (){
	let num = 0;
    
    return function (){
    	return num++;
    }
}());

 

이제 increase는 즉시 실행 함수가 리턴한 함수를 기억하고 그 함수는 자신의 상위 스코프의 식별자를 참조하고 

외부 함수보다 생명주기가 길기 때문에 클로저라 할 수 있다. 

 

이제 increase는 카운트 상태를 유지하기 위한 자유 변수 num을 언제 어디서 호출하든지 참조하고 변경할 수 있다.

또한 즉시 실행 함수는 단 한 번만 호출되기 대문에 num 변수가 초기화될 걱정도 없다.

 

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

 

* 객체 블록은 4가지 소스타입에 해당하지 않기 때문에 별도의 스코프를 생성하지 않는다.

 

const Counter = (function (){
	let num = 0;
    
    function Counter(){
    	//프로퍼티는 public 하여 은닉되지 않음.
    }
    
    Counter.prototype.increase = function (){
    	return ++num;
    };
    
    Counter.prototype.decrease = function (){
    	return num > 0 ? --num : 0;
    };
    
    return Counter;
}());

const counter = new Counter();

counter.increase();
counter.decrease();

 

num은 생성자 함수 Counter가 생성할 인스턴스의 프로퍼티가 아닌 즉시 실행 함수에서 선언된 변수로 인스턴스로 접근할 수 도 없으며 즉시 실행 함수 외부에서 접근할 수 없는 자유 변수가 된다.

 

생성자 함수인 Counter는 프로토 타입을 통해 increase, decrease 메서드를 상속받는 인스턴스를 생성한다. 이 메서드들은 모두 자신의 함수 정의가 평가되어 함수 객체가 될 때 실행 중인 실행 컨텍스트인 즉시 실행 함수의 실행 컨텍스트의 렉시컬 환경을 기억하는 클로저이다.  

따라서 프로토타입을 통해 상속되는 프로토타입 메서드 일지라도 자유 변수 num을 참조할 수 있다. 이는 num을 변경할 수 있는 메서드는 increase, decrease 밖에 없다는 이야기다.

 

가변 데이터를 피하고 불변성을 지향하는 함수형 프로그래밍에서 사이드 이펙트를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용된다.

 

또 하나 기억할 것이 있다. 클로저는 상위 스코프를 가진 외부 함수의 소멸이 조건이다. 이 말은 클로저는 독립적인 렉시컬 환경을 갖는다.  클로저를 반환하는 함수를 정의하고 여러 번 호출하면 이 클로저는 각자의 독립적인 렉시컬 환경을 가지게 된다.!

 

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

합성 함수를 통해 counter의 값을 변경시킬 수 있다. 다만 이는 makeCounter의 렉시컬 환경이 독립적으로 가지기 때문에 makeCounter를 호출할 때마다 독립적인 렉시컬 환경을 가진 클로저가 반환된다.

 

따라서 렉시컬 환경을 공유하는 클로저를 반환하도록 하여야 한다. 

const counter = (function (){
	let counter = 0;
    
    return function(aux){
    // 전달받은 함수에게 counter 변경을 위임
    	counter = aux(counter);
        return counter;
    };
}());

이는 counter를 공유 변수로 활용할 수 있는 클로저를 반환하고 이 클로저의 인자로 counter의 변경을 위임할 수 있는 보조 함수를 통해 처리할 수 있다. 

 

 

객체지향 프로그래밍 언어는 접근 제한자를 선언하여 공개 범위를 손쉽게 한정할 수 있다.

하지만 자바스크립트는 이러한 접근 제한자를 제공하지 않는다. 따라서 객체의 모든 프로퍼티와 메서드는 기본적으로 외부에 공개되어 있다. 즉 기본이 public이라는 소리이다.

 

function Person(name, age) {
    this.name = name;
    let _age = age;
    
    this.sayHi = function() {
    	console.log(`Hi ${this.name} I am {$_age}`);
    };
    
}

 

Person 생성자 함수의 name은 외부에 공개되어 있어 자유롭게 참조하거나 변경할 수 있는데, _age 변수는 Person 생성자 함수의 지역 변수로 외부에서 변경할 수 없다.

 

const me = new Person('Hi', 20);

me.sayHi();

console.log(me.name); //public 접근가능	    Hi
console.log(me._age); //private 접근 불가능 undefined

 

위 예제의 sayHi() 메서드는 인스턴스 메서드로 정의되어 Person 객체가 생성될 때마다 중복 생성된다. 

우리는 공통으로 사용될 메서드는 대개 프로토타입 메서드로 변경한다.

 

function Person(name, age) {
    this.name = name;
    let _age = age;
}

Person.prototype.sayHi = function (){
	console.log(`Hi name is ${this.name}, I am ${_age}`);
}

 

하지만 _age는 참조할 수 없다. 이는 _age는 생성자 함수의 지역변수이기 때문에 접근할 수 없다. 

이 경우 우리는 클로저를 활용해 정보 은닉을 활용 가능한 것처럼 만들 수 있다.

 

const Person = (function {
	let _age = 0;
    
    function Person(name, age){
    	this.name = name;
        _age = age;
    }
    
    Person.prototype.sayHi = function (){
    	console.log(`Hi name is ${this.name}, I am ${_age}`);
    };

	return Person; // 생성자 함수 반환
}());

 

즉시 실행 함수를 통해  _age 지역 변수에 접근할 수 있는 실행 컨텍스트의 환경에서 sayHi의 프로토타입 메서드를 정의하면 지역 변수에 접근할 수 있다. 하지만 외부에서는 _age 지역 변수에 접근할 수 없는 private 한 상태가 된다.

 

이는 Person.prototype.sayHi 메서드가 실행 종료된 이후 인스턴스들에게 호출된다고 하여도,

프로토타입 메서드는 이미 종료되어 소멸된 즉시 실행 함수의 지역 변수를 참조하는 클로저이기 때문이다.

하지만 위 코드는 _age 지역 변수의 상태가 유지되지 않는다.

 

const me = new Person('me', 20);
const you = new Person('you', 30);

me.sayHi(); // _age = 20

you.sayHi(); // _age = 30

me.sayHi(); // _age = 30 ???

 

이는 즉시 실행 함수가 실행될 때 Person.prototype.sayHi 메서드는 자신의 상위 스코프인 즉시 실행 함수의 실행 컨텍스트의 렉시컬 환경의 참조를 [[Environment]] 슬롯에 저장하고 실행 시 상위 렉시컬 환경에 상위 스코프를 저장한다.

 

따라서 Person 생성자 함수로 생성된 모든 인스턴스가 상속을 통해 호출할 수 있는 sayHi 메서드의 상위 스코프는 

어떠한 인스턴스로 호출하더라도 동일한 상위 스코프를 사용한다. 그렇기 때문에 _age 변수의 상태가 유지가 되지 않는다. 

 

이처럼 JavaScript는 정보 은닉을 완전하게 지원하지 못한다. 인스턴스 메서드를 사용한다면 자유 변수를 통해 private을 흉내 낼 수는 있지만 프로토 타입 메서드를 사용하면 이마저도 불가능하다.  이를 극복하기 위해 ES6의 Symbol(고유), WeakMap(처음 듣는다.)을 사용해 정보은닉을 흉내 냈지만 근본적인 해결책이 되지 않는다고 한다. 

 

2021년 1월에 클래스에 private 필드를 정의할 수 있는 새로운 표준 사양이 제안되어 있고 현재는 반영되어 사용 중이다. 

 

 

 

 

반응형

'JavaScript' 카테고리의 다른 글

클래스 (1)  (0) 2022.03.02
-집가서 확인  (0) 2022.03.02
클로저  (0) 2022.02.24
함수 코드 실행 컨텍스트  (0) 2022.02.23
전역 코드 실행 컨텍스트  (0) 2022.02.22

댓글