자바스크립트 딥 다이브 24장 클로저 part-2

houndhollis·2024년 4월 7일
0

월요일날 쓴거같은데 벌써 일요일이라니 4월 금방 갈꺼같다.

클로저의 활용

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

let num = 0;

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

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

위 코드는 동작은 잘하지만 오류를 일으킬 가능성이 높다!

  1. 카운트 상태는 increase 함수가 호출되기 전까지 변경되지 않고 유지되어야 한다.
  2. 이를 위해 카운트 상태는 increase 함수만이 변경할 수 있어야 한다.

하지만 카운트 상태는 전역 변수를 통해 관리되고 있기 때문에 언제든 누구나 접근할수 있다.

그렇기 때문에 아래 코드 처럼 변경되어야 한다.

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

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

이로써 의도치 않은 상태 변경은 방지했다. 이제 num 변수의 상태는 increase 함수만이 변경할수 있다. 하지만 함수가 호출될 때마다 지역변수는 0으로 다시 초기화 되기 때문에 출력 결과는 언제나 1이다.

이전 상태를 유지할 수 있도록 클로저를 사용해보자

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

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

위 코드가 실행되면 즉시 실행 함수가 반환 하는 함수 increase가 변수에 할당되고 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저 임으로 값을 기억하고 증가시키는 것을 볼수 있다.

호출된 함수는 소멸되지만 즉시 실행 함수가 반환한 클로저 increase 변수에 할당되어 호출된다. 카운트 상태를 유지하기 위한 자유 변수 num을 언제 어디서 호출하든지 참조하고 변경할수 있다.

또한 num 변수는 외부에서 직접 접근할 수 없는 은닉된 private 변수이므로 전역 변수를 사용했을 때와 같이 의도되지 않은 변경을 걱정할 필요도 없다.

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


캡슐화의 정보 은닉

캡슐화는 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것을 말한다. 캡슐화는 객체의 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉이라 한다.

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

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

const me = new Person('Lee', 20);
me.sayHi(); // Hi! My name is Lee. I am 20;

console.log(me.name); // Lee
console.log(me_age); // undefined

name 프로퍼티는 현재 외부로 공개되어 참조하거나 변경할수 있다 하지만 _age 변수는 Person 생성자 함수의 지역 변수이므로 Person 생성자 함수 외부에서 참조하거나 변경할수 없다.

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

const me = new Person('Lee', 20);
me.sayHi(); // Hi! My name is Lee. I am 20

const you = new Person('Kim, 28');
you.sayHi(); // Hi! My name is Kim. I am 28

me.sayHi(); // Hi! My name is Lee. I am 28

Person.prototype.sayHi 메서드가 단 한 번 생성되는 클로저이기 때문에 발생하는 현상이다. 메서드는 즉시 실행 함수가 호출될 때 생성된다. 이때 메서드는 자신의 상위 스코프인 즉시 실행 함수의 실행 컨텍스트의 렉시컬 환경의 참조에 저장하여 기억한다 따라서 Person 생성자 함수의 모든 인스턴스가 상속을 통해 호출할 수 있는 Person.prototype.sayHi 메서드의 상위 스코프는 어떤 인스턴스로 호출하더라도 하나의 동일한 상위 스코프를 사용한다. 이러한 이유로 Person 생성자 함수가 여러 개의 인스턴스를 생성할 경우 _age 변수의 상태가 유지 되지않는다.


자주 발생하는 실수

아래는 클로저를 사용할 때 자주 발생할 수 있는 실수를 보여주는 예제다.

var funcs = [];

for (var i = 0; i < 3; i++) {
	funcs[i] = function() { return i };
}

for (var j = 0; i < funcs.length; j++) {
	console.log(funcs[j]());
}

아래 반복문 까지 돌면 console에 뭔가 0,1,2 를 반환할 것으로 기대되지만 그렇지 않다.
for 문의 변수 선어문에서 var 키워드로 선언된 i 변수는 블록 레벨 스코프가 아닌 함수 레벨 스코프를 갖기 때문에 전역 변수다. 전역 변수 i에는 0,1,2 가 순차적으로 할당된다. 따라서 funcs 배열의 요소로 추가한 함수를 호출하면 전역 변수 i를 참조하여 i의 값이 3이 출력된다.

클로저를 사용해 위 예제를 바르게 동작하는 코드로 변경 한다면

var funcs = [];

for (var i = 0; i < 3; i++){
	funcs[i] = (function(id){
    	return function() {
        	return id;
        }
    }(i));
}

for (var j = 0; i < funcs.length; j++) {
	console.log(funcs[j]());
}

즉시 실행 함수는 전역 변수 i 에 현재 할당되어 값을 인수로 전달받아 id에 할당한 후 중첩 함수를 반환하고 종료한다. 이떄 즉시실행 함수의 매개변수 id는 즉시 실행 함수가 반환한 중첩 함수의 상위 스코프에 존재한다. 즉시 실행 함수가 반환한 중첩 함수는 자신의 상위 스코프를 기억하는 클로저이고 매개변수 id는 즉시실행 함수가 반환한 중첩 함수에 묶여잇는 자유 변수가 되어 값이 유지된다.

하지만 이것도 ES6의 let 키워드를 사용하면 이 같은 번거로움이 깔끔하게 해결된다.

또 다른 방법으로 함수형 프로그래밍 기법인 고차함수를 사용하는 방법도 있다.

const funcs = Array.from(new Array(3), (_, i) => () => i); // (3) [f,f,f];

funcs.forEach(f => console.log(f())); // 0,1,2

오늘은 여기까지! 클로저는 몇번은 더 보면 좋은 내용 같다.

profile
한 줄 소개

0개의 댓글