TIL 76 | JS Closure(2) 활용과 주의사항

Gom·2021년 5월 22일
1

JavaScript

목록 보기
20/22
post-thumbnail

Closure(1) 클로저는 왜 클로저일까?

* Closure의 활용

클로저가 가장 처음 등장했을 때의 개념으로 판단하자면 자바스크립트의 모든 함수는 클로저이다. 함수를 만들고 그 함수 내부의 코드가 탐색하는 스코프를 함수 생성 당시의 lexical scope로 고정하면 바로 클로저가 되는 것이기 때문이다.

그러나 실제로 자바스크립트의 모든 함수를 전부 클로저라고 부르지는 않는다.

일반적으로 외부 접근을 제한하는 비공개 변수를 가질 수 있는 환경에 있는 경우를 클로저라고 한다. 그러한 경우에 이전 편에서 언급한 클로저를 사용하는 이유에 맞게 활용이 가능하기 때문이다.

클로저의 용도 요약 : 정보 은닉과 캡슐화

1. 클로저를 이용한 은닉화

1-1. IIFE를 이용

var counter = (function() {
  var privateCounter = 0;function changeBy(val) {    │ increment, decrement, value
    privateCounter += val;    │ 세 메소드가 공유하는 private items
  }return {
    increment: function() {   
      changeBy(1);	      
    },		
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };
})();

console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1

코드설명

익명함수가 즉시 실행되며 counter 변수에 함수의 반환 값이 할당된다.
익명함수의 반환 값인 세 함수는 동일한 lexical environment를 공유하고 있으며 privateCounterchangeBy()에 유일하게 접근할 수 있는 클로저이다.

  • public 함수 = 특권 메소드 = 공개멤버
    counter.increment
    counter.decrement
    counter.value
  • 비공개멤버
    privateCounter
    changeBy

1-2. 기명함수를 이용

IIFE대신 기명함수를 이용하면 독립성을 유지하는 다수의 카운터를 생성할 수 있다.

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
};

var counter1 = makeCounter();
var counter2 = makeCounter();
alert(counter1.value()); /* 0 */
counter1.increment();
counter1.increment();
alert(counter1.value()); /* 2 */
counter1.decrement();
alert(counter1.value()); /* 1 */ ┐ counter1과 counter2는
alert(counter2.value()); /* 0 */ ┘ 서로 다른 독립된 클로저를 가진다.

코드설명

각 클로저는 그들 고유의 클로저를 통한 privateCounter 변수의 다른 버전을 참조한다.
하나의 클로저에서 변수 값을 변경해도 다른 클로저의 값에는 영향을 주지 않는다.

2. Currying을 이용한 템플릿화

Currying은 여러 개의 인자를 받는 함수를 단일 인자를 받는 함수의 체인을 이용하는 방식으로 바꾸는 것을 의미한다. 기명함수를 이용한 위의 예시에 currying을 접목해 클로저 함수의 외부 함수를 템플릿처럼 활용할 수 있다.

function greetCurried (greeting) {
  return function (name) {
    console.log(greeting + ", " + name);
  }
}

greetCurried("Hi there")("Howard"); //"Hi there, Howard"

let greetHello = greetCurried("Hello");
greetHello("Heidi"); //"Hello, Heidi"
greetHello("Eddie"); //"Hello, Eddie"


let greetGoodmorning = greetCurried("Good morning");

greetGoodmorning('wendy');	 // Good morning, wendy
greetGoodmorning('honey'); // Good morning, honey

* 사용 시 주의점

성능과 메모리 문제

  • 클로저는 Lexical environment를 기억하기 위해 메모리를 소모한다. 내부 변수가 차지하는 메모리를 GC가 자동으로 회수하지 않으므로 클로저 사용이 끝났다면 참조를 제거하여 자바스크립트로 하여금 메모리를 회수해가도 된다는 것을 알려주어야 메모리 낭비를 방지할 수 있다.
  • scope chain을 거슬러 올라가는 행동을 하므로 조금 느리다.

반복문 클로저

클로저에 대한 이해가 부족한 상태라면 반복문과 클로저가 함께 쓰였을 때 예상과 다른 결과가 발생하게 된다. 이전 편에 첫번째로 제시되었던 문제를 다시 가져왔다.

function count() {
    var i;
    for (i = 0; i < 5; i++) {
      console.log(i)
        setTimeout(function() {
            console.log(i)
        }, i*1000)
    }
}
count();

작성자의 의도 💭 : 0,1,2,3,4가 1초 간격으로 출력되겠군 !
실제 출력결과 : 5,5,5,5,5

setTimeout뿐 아니라 이벤트 리스너 등 반복문 순회 이후 실행될 수 있는 클로저 함수는 유의해야 한다. 의도대로 코드가 작동하게 하려면 아래와 같은 방법으로 해결할 수 있다.

해결방안 1. IIFE + parameter

원리
setTimeout함수 바깥으로 새로운 스코프를 만들고 IIFE로 i를 인자로 넘긴다.
i 증가 시마다 고정된 countingNumber에 대한 독립적인 클로저 함수가 만들어진다.
독립된 lexical environment를 가지기에 각 시점의 countingNumber에 접근하여 출력할 수 있다.

 function count() {
     var i;
     for (i = 0; i < 5; i++) {
         (function(countingNumber) {
             setTimeout(function() {
            console.log(countingNumber)
        }, i*1000)
         })(i);
     }
 }
 count(); //0,1,2,3,4

(+) setTimeout이 아닌 경우 새로운 스코프를 만들지 않고 IIFE를 적용하는 것만으로 문제가 해결되기도 한다. 그러나 setTimeout은 콜백으로 넘기는 함수 자체에 IIFE를 적용하면 두번째 인자로 주어지는 딜레이가 작동하지 않게 되므로 바깥에 새로운 스코프를 만들어 적용할 수 밖에 없다.

해결방안 2. ES6에서 추가된 Block Scope

원리
var는 함수 수준, let은 블록 수준의 스코프를 가진다.
즉 let키워드가 사용된 블록에 대해 lecxical envireonment가 새롭게 생성된다.
반복문이 실행될 때마다 클로저가 생성되므로 i는 각 시점의 i에 접근하여 증가하는 숫자를 출력할 수 있다.

function count() {
    'use strict';
    for (let i = 0; i < 5; i++) {
        setTimeout(function timer() {
            console.log(i);
        }, i*1000);
    }
}
count(); //0,1,2,3,4

참고자료
JavaScript 클로저(Closure)
자바스크립트 공부 // 스코프(Scope), 클로저(Closure), 즉시실행함수(IIFE)

profile
안 되는 이유보다 가능한 방법을 찾을래요

0개의 댓글