지금까지 클로저의 동작원리에 대해서 알아보았다. 상위 스코프의 식별자를 외부 함수의 생명 주기 소멸 이후에도 참조한다는 개념을 어디다 사용해야 효과적으로 클로저를 활용했다고 하는 것일까?
클로저는 상태를 안전하게 변경하고 유지하기 위해서 사용한다.
즉 상태를 외부로부터 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.
아. 캡슐화? 전에서 클로저는 함수가 자유 변수에 대해 닫혀있다는 의미라고 했다. 자유 변수 즉 상태에 대해 묶여있는 함수이구나라고 생각된다.
요청사항을 하나 만들어보자. 요청은 함수 호출 시 호출된 횟수를 누적하여 출력하는 것이다.
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 |
댓글