본문 바로가기
공부/JS

[Javascript] 클로저 알아보기 - 2

by Piva 2024. 1. 8.
  • 지난 게시물에 이어서 클로저의 사용 예시에 대해 정리한다.
  • 주로 코어 자바스크립트 책을 참고했다.

클로저 활용 사례

1. 콜백 함수 내부에서 외부 데이터 사용 시

콜백 함수란 다른 함수에 인자로 전달되는 함수이다. 즉, 다른 함수에서 이 콜백 함수를 사용하는 주도권을 쥐게 된다. 이러한 콜백 함수에서, 외부 변수를 참조하는 데에는 크게 3가지가 있다.

 

(a) 콜백함수 내부에서 외부 변수를 직접 참조하는 방법(클로저 사용)

var nameList = ['Ann', 'Gren', 'Jennifer'];
var $ul = document.createElement('ul');

nameList.forEach(function (name) {
	var $li = document.createElement('li');
    $li.innerText = name;
    $li.addEventListener('click', function () {
    	console.log('Hello', name);
    });
    $ul.appendChild($li);
});

document.body.appendChild($ul);
  • forEach 문에서, addEventListener의 인자로 전달하는 콜백 함수를 살펴보면, 바깥쪽에 정의된 name을 참조하여 사용하고 있음을 알 수 있다.
  • 따라서 forEach 문이 종료된 후에도 콜백 함수 내에서는 여전히 forEach 문의 LexicalEnvironment를 참고할 것이고, 이에 따라 name은 가비지 컬렉팅 당하지 않을 것이다.

 

(b) bind를 사용하는 방법(클로저 사용 X)

  위 코드에서 console 부분의 별도의 함수로 빼낼 경우, 아래와 같다.

var nameList = ['Ann', 'Gren', 'Jennifer'];
var $ul = document.createElement('ul');

var sayName = function (name) {
	console.log('Hello', name);
};

nameList.forEach(function (name) {
	var $li = document.createElement('li');
    $li.innerText = name;
    $li.addEventListener('click', sayName);
    $ul.appendChild($li);
});

document.body.appendChild($ul);
  • 이 경우, 이전 코드와 달리 각 항목을 클릭해도 이름이 나오지 않고 Event 객체가 출력된다.
  • 이는 addEventListener가 콜백 함수의 인자로 이벤트 객체를 전달하기 때문이다.
  • 따라서 이 경우 bind를 사용해 의도한 동작을 구현할 수 있다.

 

var nameList = ['Ann', 'Gren', 'Jennifer'];
var $ul = document.createElement('ul');

const sayName = function (name) {
	console.log('Hello', name);
};

nameList.forEach(function (name) {
	var $li = document.createElement('li');
    $li.innerText = name;
    // bind를 사용하여 콜백 함수에서 사용할 인자를 직접 넘겨준다
    $li.addEventListener('click', sayName.bind(null, name));
    $ul.appendChild($li);
});

document.body.appendChild($ul);

 

  이 경우 원래 콜백 함수에 인자로 전달될 예정이던 이벤트 객체가 두 번째 인자로 전달될 것이라는 점, this가 원래 값과 달라지는 점 등의 문제점이 존재한다.

 

 

(c) 고차 함수를 사용하는 방법(클로저 사용)

고차 함수 (Higher Order Function): 함수를 인자로 받거나 함수를 리턴하는 함수.
- "함수를 리턴한다" === "클로저를 반환한다"

 

  고차함수를 사용하여 위의 코드를 수정하면 아래와 같다.

var nameList = ['Ann', 'Gren', 'Jennifer'];
var $ul = document.createElement('ul');

// 인자를 받아 익명 함수(클로저)를 반환하는 고차 함수
var sayNameBuilder = function (name) {
	return function () {
    	console.log('Hello', name);
    };
};

nameList.forEach(function (name) {
	var $li = document.createElement('li');
    $li.innerText = name;
    
    $li.addEventListener('click', sayNameBuilder(name));
    $ul.appendChild($li);
});

document.body.appendChild($ul);
  • 기존의 sayName 함수를, name을 인자로 받아 그 name을 console을 통해 출력하는 클로저를 반환하는 고차 함수로 바꾸었다.
  • 이렇게 반환된 클로저는 addEventListener의 콜백함수로 전달된다.

 

 

2. 접근 권한 제어(정보 은닉)

정보 은닉(Information hiding)

: 외부에 공개될 필요가 없는 구현 일부를 감추어 객체의 상태 변경을 보호하는 것(캡슐화).

※ 캡슐화(Encapsulation): 객체의 상태를 나타내는 프로퍼티와, 이 프로퍼티들을 참조/조작할 수 있는 메서드를 하나로 묶는 것.
  • JAVA나 C 등의 언어에서는 접근 제한자(private, public 등...)를 사용하여 공개할 속성과 그렇지 않은 속성을 구분할 수 있다.
  • 하지만 자바스크립트에는 접근 제한자가 존재하지 않으며, 일반적으로 객체의 메서드와 프로퍼티는 public하다.
  • 그러나 클로저를 사용하면 접근 권한 제어를 구현하는 것이 가능하다!
var foo = function () {
	var counter = 1;
    
    var bar = function () {
    	return ++counter;
    };
    
    return bar;
};

var foo2 = foo();
console.log(foo2());
  • foo 함수가 종료될 때 내부 함수인 bar를 반환하고, 반환된 내부 함수는 foo2를 통해 호출할 수 있다. 이를 통해 foo 함수 내부의 지역변수인 counter에 접근하는 것이 가능하다.
  • 즉, 공개하고자 하는(접근을 허용하고자 하는) 정보를 return 을 통해 반환함으로써 바깥에서 접근하게 만드는 것이 가능하다.
    • return한 변수는 공개 멤버(Public), 그렇지 않은 변수들은 비공개 멤버(Private)가 된다.

 

 

3. 부분 적용 함수(Partially applied function)

: n개의 인자를 받는 함수에 일부 인자만 넘겨 기억시켰다가, 나중에 나머지 인자를 넘기면 비로소 실행되는 함수.

  • bind 함수가 이러한 부분 적용 함수를 만드는 데 잘 사용된다. (참고)
var add = function () {
	var result = 0;
    for (var i = 0; i < arguments.length; i++) {
    	result += arguments[i];
    }
    return result;
};

// bind 함수를 사용하여 add 함수에 1부터 5까지의 인수를 미리 전달한다
var addPartial = add.bind(null, 1, 2, 3, 4, 5);

// 후에 6부터 10까지의 나머지 인수를 전달한다
console.log(addPartial(6, 7, 8, 9, 10));

 

 

4. 커링 함수(Currying function)

: 여러 개의 인자를 받는 함수를 하나의 인자로 받는 함수로 나눠, 순차적으로 호출할 수 있도록 한 함수.

  • 부분 적용 함수와 비슷하지만, 오직 한 번에 하나의 인자만 전달한다는 원칙이 있다.
var curryingFunc = function (func) {
    return function (a) {
        return function (b) {
            return function (c) {
                return func(a, b, c);
            };
        };
    };
};

// ES6 문법을 사용하면 가독성을 높일 수 있다
var curryingFunc = func => a => b => c => func(a, b, c);
  • 함수에서 필요로하는 모든 인자가 넘겨질 때까지 함수가 실행되지 않다가, 모든 인자가 전달되면 그제서야 함수가 실행될 것이므로, 지연 실행(Lazy Execution)이 일어난다.

 


  • 클로저의 정의를 이해하는 것까진 괜찮았는데, 아직 사용 예시가 어려운 느낌이다.
  • 더 익숙해질 때까지 반복적으로 실제 사용 예시를 참고해봐야겠다...