본문 바로가기

CS/모던 자바스크립트 Deep Dive

24장 클로저(2)

반응형

2023년 4월 21일 401p~416p

 

24.4 클로저의 활용

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

상태를 안전하게 은닉(information hiding)하고 특정 함수에게만 상태 변경을 허용하기 위해 사용된다.

1.전역 변수로 관리

// 카운트 상태 변수
let num = 0;

// 카운트 상태 변경 함수
const increase = function () {
  // 카운트 상태를 1만큼 증가 시킨다.
  return ++num;
};

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

위 코드는 오류의 가능성이 있어 좋지 않은 코드다. num 전역 변수에 언제든지 누구나 접근하여 변경이 가능하기 때문이다.

2.함수의 지역 변수로 관리

// 카운트 상태 변경 함수
const increase = function () {
  // 카운트 상태 변수
  let num = 0;

  // 카운트 상태를 1만큼 증가 시킨다.
  return ++num;
};

// 이전 상태를 유지하지 못한다.
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1

지역 변수로 활용하여 첫 번째 예제보다는 좋아 보이지만 함수가 호출될 때마다 지역 변수 num은 다시 선언되어 0으로 초기화되기 때문에 상태를 유지하지 못한다.

3.클로저 활용

// 카운트 상태 변경 함수
const increase = (function () {
  // 카운트 상태 변수
  let num = 0;

  // 클로저
  return function () {
    // 카운트 상태를 1만큼 증가 시킨다.
    return ++num;
  };
}());

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

 위 코드는 실행되면 즉시 실행 함수가 호출되고 즉시 실행 함수가 반환한 함수가 increase 변수에 할당된다. increase 변수에 할당된 함수는 자신이 정의된 위치에 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저다. 즉시 실행 함수가 반환한 클로저는 카운트 상태를 유지하기 위한 자유 변수 num을 언제 어디서 호출하든지 참조하고 변경할 수 있다.

즉시 실행 함수는 한번만 실행되므로 increase가 호출될 때마다 num 변수가 초기화 될 일은 없다. 클로저로 num의 상태를 은닉하여 의도치 않게 변경되지 않도록 안전하게 관리하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지한다.

4.감소 기능 추가(클로저)

const counter = (function () {
  // 카운트 상태 변수
  let num = 0;

  // 클로저인 메서드를 갖는 객체를 반환한다.
  // 객체 리터럴은 스코프를 만들지 않는다.
  // 따라서 아래 메서드들의 상위 스코프는 즉시 실행 함수의 렉시컬 환경이다.
  return {
    // num: 0, // 프로퍼티는 public하므로 은닉되지 않는다.
    increase() {
      return ++num;
    },
    decrease() {
      return num > 0 ? --num : 0;
    }
  };
}());

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

console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0

5.생성자 함수로 표현

increase, decrease 메서드는 프로토타입 메서드이다. 이 메서드들이 평가되어 함수 객체가 생성될 때 실행 중인 실행 컨텍스트는 즉시 실행 함수의 실행 컨텍스트이다. 따라서 increase, decrease메서드는 즉시 실행 함수의 실행 컨텍스트의 렉시컬 환경을 기억하는 클로저 이다.

다시 말하면 num변수의 값은 increase와 decrease메서드만이 변경할 수 있다.

const Counter = (function () {
  // ① 카운트 상태 변수
  let num = 0;

  function Counter() {
    // this.num = 0; // ② 프로퍼티는 public하므로 은닉되지 않는다.
  }

  Counter.prototype.increase = function () {
    return ++num;
  };

  Counter.prototype.decrease = function () {
    return num > 0 ? --num : 0;
  };

  return Counter;
}());

const counter = new Counter();

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

console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0

6.고차 함수 활용한 클로저

* 고차 함수 : 보조 함수를 아규먼트로 전달받고 함수를 반환하는 함수

// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
function makeCounter(predicate) {
  // 카운트 상태를 유지하기 위한 자유 변수
  let counter = 0;

  // 클로저를 반환
  return function () {
    // 인수로 전달 받은 보조 함수에 상태 변경을 위임한다.
    counter = predicate(counter);
    return counter;
  };
}

// 보조 함수
function increase(n) {
  return ++n;
}

// 보조 함수
function decrease(n) {
  return --n;
}

// 함수로 함수를 생성한다.
// makeCounter 함수는 보조 함수를 인수로 전달받아 함수를 반환한다
const increaser = makeCounter(increase); // ①
console.log(increaser()); // 1
console.log(increaser()); // 2

// increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동하지 않는다.
const decreaser = makeCounter(decrease); // ②
console.log(decreaser()); // -1
console.log(decreaser()); // -2

const increaser = makeCounter(increase); // ①에서 makeCounter가 처음 호출되었을 때 

imakeCounter 함수가 반환한 함수가 increase에 할당된 후 makeCounter 함수의 실행 컨텍스트는 소멸되지만, makeCounter 함수 실행 컨텍스트의 렉시컬 환경makeCounter함수가 반환한 함수의 [[Environment]] 내부 슬롯에 의해 참조되고 있기 때문에 소멸되지 않는다.

const decreaser = makeCounter(decrease); // ②에서 makeCounter가 두 번째 호출되었을 때

makeCounter함수를 호출할 때 마다 새로운 독립된 렉시컬 환경을 갖기 때문에, 자유 변수 counter를 공유하지 못해 카운터의 증감이 연동되지 않는다.

카운터 증감을 연동하고 싶다면 아래 예제처럼 해야 한다.

// 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
const counter = (function () {
  // 카운트 상태를 유지하기 위한 자유 변수
  let counter = 0;

  // 함수를 인수로 전달받는 클로저를 반환
  return function (aux) {
    // 인수로 전달 받은 보조 함수에 상태 변경을 위임한다.
    counter = aux(counter);
    return counter;
  };
}());

// 보조 함수
function increase(n) {
  return ++n;
}

// 보조 함수
function decrease(n) {
  return --n;
}

// 보조 함수를 전달하여 호출
console.log(counter(increase)); // 1
console.log(counter(increase)); // 2

// 자유 변수를 공유한다.
console.log(counter(decrease)); // 1
console.log(counter(decrease)); // 0

24.5 캡슐화와 정보은닉

캡슐화(encapsulation)는 객체의 상태(state)를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것을 말한다.

캡슐화는 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉(information hiding)이라 한다.

정보 은닉은 외부에 공개할 필요가 없는 구현의 일부를 외부에 공개되지 않도록 감추어 적절치 못한 접근으로부터 객체의 상태가 변경되는 것을 방지해 정보를 보호하고, 객체 간의 상호 의존성, 즉 결합도(coupling)를 낮추는 효과가 있다.

대부분의 객체지향 프로그래밍 언어는 public, private, protected와 같은 접근 제한자(access modifier)를 제공하지만 자바스크립트는 제공하지 않는다. 즉 모든 프로퍼티와 메서드는 public이다.

1. 인스턴스 메서드에서 지역 변수 참조

_age 변수는 Person 생성자 함수의 지역 변수 이므로 Person 생성자 함수 외부에서는 지역 변수인 _age를 참조하거나 변경할 수 없다.

function Person(name, age) {
  this.name = name; // public
  let _age = age;   // private

  // 인스턴스 메서드
  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

const you = new Person('Kim', 30);
you.sayHi(); // Hi! My name is Kim. I am 30.
console.log(you.name); // Kim
console.log(you._age); // undefined

위 예제에서의 sayHi 메소드는 인스턴스 메서드 이므로 Person 객체가 생성될 때마다 중복 생성된다. 이 메서드를 프로토타입 메서드로 변경하여 중복 생성을 방지해보자.

2. 프로토타입 메서드에서 지역 변수 참조

프로토타입 메서드 내에서는 지역 변수인 _age를 참조할 수 없다.

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

// 프로토타입 메서드
Person.prototype.sayHi = function () {
  // Person 생성자 함수의 지역 변수 _age를 참조할 수 없다
  console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
};

3. 프로토타입 메서드를 클로저로 활용

즉시 실행 함수를 사용해 함수 내부에 지역 변수 _age를 선언한 뒤, 그 안에 Person 생성자 함수와 프로토타입 메서드를 선언하고 생성자 함수를 반환하도록 한다.

그렇게되면 즉시 실행 함수는 종료되었지만 반환된 Person생성자 함수는 여전히 지역 변수_age를 참조할 수 있는 클로저이다.

const Person = (function () {
  let _age = 0; // private

  // 생성자 함수
  function Person(name, age) {
    this.name = name; // public
    _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.
console.log(me.name); // Lee
console.log(me._age); // undefined

const you = new Person('Kim', 30);
you.sayHi(); // Hi! My name is Kim. I am 30.
console.log(you.name); // Kim
console.log(you._age); // undefined

하지만 위 예제도 완벽하진 않다. Person생성자 함수가 여러 개의 인스턴스를 생성할 경우 _age변수의 상태가 유지되지 않는다. 이는 Person.prototype.sayHi 메서드가 단 한 번 생성되는 클로저이기 때문에 발생하는 현상이다. Person 생성자 함수의 모든 인스턴스가 상속을 통해 호출할 수 있는 Person.prototype.sayHi 메서드의 상위 스코프는 어떤 인스턴스로 호출하더라도 하나의 동일한 상위 스코프를 사용하게 된다(즉시 실행 함수). 이러한 이유로 여러 개의 인스턴스가 생성되면 위와 같이 변수의 상태가 유지되지 않는다.

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

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

// _age 변수 값이 변경된다!
me.sayHi(); // Hi! My name is Lee. I am 30.

 

24.6 자주 발생하는 실수

var를 이용한 for문을 사용할 때 for문 내부에 함수를 선언하는 경우 기대하지 않은 결과가 나타난다.

var funcs = [];

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

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

0 1 2가 출력될 것을 기대하였지만 3 3 3이 출력된다. 이유는 var로 선언된 i변수는 전역 변수로 선언되었기 때문에 함수 선언문에 i 값을 전달하였더라도, 호출하는 시점에는 i값이 3이기 때문에 3 3 3이 출력된다.

위 예제를 클로저를 사용해 원하는 결과값을 얻기 원한다면 다음과 같이 바꿔야 한다.

var funcs = [];

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

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

for문 내부에 함수를 선언할 때 즉시 실행 함수로 감싸고 i값을 전달해 주도록 수정하였다. 1에서 즉시 실행 함수는 전역 변수 i에 현재 할당되어 있는 값을 아규먼트로 전달받아 파라미터 id에 할당한 후 중첩 함수를 반환하고 종료된다. 즉시 실행 함수가 반환한 함수는 funcs 배열에 순차적으로 저장된다. 이때 즉시 실행 함수의 파라미터 id는 즉시 실행 함수가 반환한 중첩 함수의 상위 스코프에 존재한다.

즉시 실행 함수가 반환한 중첩함수는 클로저이므로, 파라미터 id는 즉시 실행 함수가 반환한 중첩함수에 묶여있는 자유 변수가 되어 그 값이 유지된다.

위 예제 보다 더욱 간단하게 하는 방법은 ES6의 let 키워드를 사용하는 것이다.

const funcs = [];

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

for (let i = 0; i < funcs.length; i++) {
  console.log(funcs[i]()); // 0 1 2

let 키워드는 블록 레벨 스코프를 따르기 때문에 for문이 반복될 때 마다 새로운 렉시컬 환경이 생성된다. 

for문의 변수 선언문에서 let을 사용하면 코드 블록이 반복 실행 될 때마다 새로운 렉시컬 환경을 생성한다. 만약 for문의 코드 블록 내에서 정의한 함수가 있다면 이 함수의 상위 스코프for문의 코드 블록이 반복 실행될 때마다 생성된 for문 코드 블록의 새로운 렉시컬 환경이다. 이것을 통해 각 반복문마다 증가되는 i값을 funcs 요소가 되는 함수에 적절히 순서대로 넣어줄 수 있는 것이다.

단, 이것도 반복문의 코드 블록 내부에서 함수를 정의할 때에만 의미가 있다. 반복문의 코드 블록 내부에 함수 정의가 없는 경우 참조할 대상이 없기 때문에 가비지 컬렉션의 대상이 된다. 

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

// 요소가 3개인 배열을 생성하고 배열의 인덱스를 반환하는 함수를 요소로 추가한다.
// 배열의 요소로 추가된 함수들은 모두 클로저다.
const funcs = Array.from(new Array(3), (_, i) => () => i); // (3) [ƒ, ƒ, ƒ]

// 배열의 요소로 추가된 함수 들을 순차적으로 호출한다.
funcs.forEach(f => console.log(f())); // 0 1 2

Array.from메서드의 첫 번째 파라미터에는 iterable 혹은 유사배열이 오고, 두 번째 파라미터에는 callback함수를 넣어준다. callback함수의 첫 번째 파라미터는 element값, 두 번째 파라미터는 index이다.

Array.from([1,2,3], (v,i)=>v*2)
// [2, 4, 6]

// 'foo'는 유사배열
Array.from('foo', (v,i)=>v)
// ['f', 'o', 'o']

Array.from('foo', (v,i)=>v+i)
// ['f0', 'o1', 'o2']

// 함수를 반환
Array.from('foo', (v,i)=>()=>v)
// [ƒ, ƒ, ƒ]

 

반응형

'CS > 모던 자바스크립트 Deep Dive' 카테고리의 다른 글

25장 클래스(2)  (0) 2023.04.25
25장 클래스(1)  (0) 2023.04.24
24장 클로저(1)  (0) 2023.04.20
15장 let,const 키워드와 블록 레벨 스코프  (0) 2023.04.19
14장 전역 변수의 문제점  (0) 2023.04.18