[JS] 클로저

서로·2024년 10월 17일
0

JS

목록 보기
10/15
post-thumbnail

➊ 클로저

클로저란 어떤 함수에서 선언한 변수를 참조하는 내부 함수에서만 발생하는 현상이다.

다른 책에서는 클로저를 다음과 같이 정의한다.

  • 자신을 내포하는 함수의 컨텍스트에 접근할 수 있는 함수
  • 함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에서 정의하는 것
  • 함수를 선언할 때 만들어지는 유효 범위가 사라진 후에도 호출할 수 있는 함수
  • 이미 생명 주기상 끝난 외부 함수의 변수를 참조하는 함수
  • 로컬 변수를 참조하고 있는 함수 내의 함수
  • 자신이 생성될 때의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행될 때 사용할 변수들만을 기억하여 유지시키는 함수

위에서 정의된 문장들은 간결하지만 클로저가 정확히 무엇인지 이해하기 어렵게 만든다.

아래 예시를 통해 클로저에 대해 제대로 이해해보자!

① return을 통해 외부 함수의 변수 참조

var outer = function () {
    var a = 1;
    var inner = function () {
        return ++a;
    };
    return inner;
};
var outer2 = outer();
console.log(outer2());  // 2
console.log(outer2());  // 3

위의 코드에서 outer 함수 내부에 inner 함수를 정의하였다.
이때 inner 함수 내부에서 a 변수를 사용하고 있는데,
a 변수는 스코프 체인을 통해 검색한 outer 함수에 등록된 식별자이다!
따라서 inner 함수에 등록된 변수라고 볼 수 없다.

var outer2 = outer(); 이 코드에서 outer 함수의 리턴값을 outer2 변수에 할당한다.
outer 함수의 리턴값은 inner 함수 그 자체이다!
따라서 outer 함수가 콜 스택에서 제거되어도 inner 함수는 outer2 변수가 참조하고 있기 때문에 언제든지 호출 가능하다.

그러나 원래대로면 inner 함수는 a 변수를 사용할 수 없을 것이다.
왜냐하면 a 변수가 등록되어 있는 outer는 콜 스택에서 이미 제거되어 LexicalEnvironment가 없기 때문이다.

하지만 outer2 즉, inner 함수가 실행되면 정상적으로 a의 값이 출력된다.
이처럼 외부 함수가 종료되었음에도 외부 함수의 변수를 사용할 수 있는 현상을 클로저라고 한다!

이를 바탕으로 클로저의 정의를 다시 고쳐보면 다음과 같다.

클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부 함수 B를 외부로 전달할 경우
A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상이다.

② 콜백 함수를 통해 외부 함수의 변수 참조

return 없이도 클로저가 발생하는 다양한 경우가 있는데, 대표적으로 아래의 예시가 있다.

// (1) setInterval/setTimeout
(function () {
    var a = 0;
    var intervalId = null;
    var inner = function () {
        if (++a >= 10) {
            clearInterval(intervalId);
        }
        console.log(a);
    };
    intervalId = setInterval(inner, 1000);
})(); // 즉시 실행 함수

위의 예시에서 콜백 함수로 전달된 inner 함수는 a 변수를 사용하고 있다.
하지만 a 변수는 위의 즉시 실행 함수 내부에서 정의되었으므로 즉시 실행 함수가 콜 스택에서 제거되면
a 변수가 등록되어 있는 LexicalEnvironment 또한 사라질 것이다.

그러나 1초마다 정상적으로 a 변수를 출력할 수 있게 되는데
이는 아직 a 변수를 참조하고 있는 함수인 inner가 존재하기 때문에
가비지 컬렉터의 수거 대상이 아니기 때문이다!

다른 예시를 살펴보자.

// (2) eventListener
(function () {
    var count = 0;
    var button = document.createElement('button');
    button.innerText = 'click';
    button.addEventListener('click', function () {
        console.log(++count, 'times clicked');
    });
    document.body.appendChild(button);
})();

위의 예시에서 addEventListener의 익명 콜백 함수가 전달되었다.
이 콜백 함수는 button을 클릭할 때마다 count를 출력하도록 하는데,
count 변수는 콜백 함수 내부에서 선언된 변수가 아니라
그 외부 함수(즉시 실행 함수)에 등록된 변수이다.
따라서 즉시 실행 함수가 콜 스택에서 제거되면 원래대로면 count 변수를 참조할 수 없을 것이다.
하지만 버튼을 클릭하면 정상적으로 count가 증가하면서 출력된다.

이와 같이 외부 함수가 종료되었음에도 그 외부 함수의 변수에 접근할 수 있는 현상클로저라고 한다.

➋ 클로저 활용

① 고차 함수 사용

콜백 함수를 고차 함수로 바꿔서 클로저를 적극적으로 활용하는 방안이 존재한다.

고차 함수란 함수를 인자로 받거나 함수를 리턴하는 함수이다.

var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');

var alertFruitBuilder = function (fruit) {
    return function () {
        alert('your choice is ' + fruit);
    };
};

fruits.forEach(function (fruit) {
    var $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', alertFruitBuilder(fruit));
    $ul.appendChild($li);
});

document.body.appendChild($ul);

li에 클릭 이벤트가 발생할 때마다 호출되는 함수는 alertFruitBuilder의 리턴값이다!
alertFruitBuilder의 리턴값은 다음과 같은 함수이다.

function () {
    alert('your choice is ' + fruit);
};

이 익명 함수 내부에서는 fruit가 정의되어 있지 않다.
하지만 이 외부 함수에 fruit가 정의되어 있기 때문에(정확히 말하면 인자로 전달받았기 때문에)
클로저 현상이 발생하여 fruit을 출력할 수 있다!

🤔 왜 익명 함수를 그대로 전달하지 않고 고차 함수를 사용한 것일까?

왜냐하면 addEventListener은 콜백 함수의 첫 번째 인자로 이벤트 객체를 전달하도록 정의되어 있기 때문이다.
첫 번째 인자를 이벤트 객체가 아니라 fruit을 전달해주고 싶어서 클로저를 사용한 예이다.

② 접근 권한 제어(정보 은닉) 🌟

정보 은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서
모듈 간의 결합도를 낮추고 유연성을 높이고자 하는 개념이다.

접근 권한에는 흔히 다음과 같은 세 가지가 존재한다.

  • public
  • protected
  • private

자바스크립트에서는 접근 권한을 직접적으로 부여하도록 설계되어 있진 않지만
클로저를 이용하여 접근 권한 제어를 할 수 있다.

아래의 예시를 살펴보자.

var createCar = function () {
    var fuel = Math.ceil(Math.random() * 10 + 10);
    var power = Math.ceil(Math.random() * 3 + 2);
    var moved = 0;

    // 외부로 공개할 함수 (fuel, moved에 접근할 수 있기 때문에 클로저 발생)
    return {
        get moved () {
            return moved;
        },
        run: function () {
            var km = Math.ceil(Math.random() * 6);
            var wasteFuel = km / power;
            if (fuel < wasteFuel) {
                console.log('이동불가');
                return;
            }
            fuel -= wasteFuel;
            moved += km;
            console.log(km + 'km 이동 (총 ' moved + 'km). 남은 연료: ' + fuel);
        }
    };
};

var car = createCar();

위의 createCar 함수는 moved getterrun 함수를 객체로 묶어서 반환한다.
따라서 변수 car은 이 리턴값인 moved, fuel에 접근할 수 있다.
그러나 moved getterrun 함수만을 이용하여 접근만 가능할 뿐
그 외의 방법으로 fuel, power, move을 접근 및 조작할 수 없다 🚨

이처럼 외부에 제공하고자 하는 정보들을 모아서 return 하고,
내부에서만 사용할 정보들은 return 하지 않는 것으로 접근 권한 제어가 가능하다!

return한 변수들은 public 멤버가 되고, 그렇지 않은 변수들은 private 멤버가 된다.

③ 부분 적용 함수

클로저를 사용하여 부분 적용 함수를 구현할 수 있다.

부분 적용 함수란 n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가,
나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수이다.

부분 적용 함수를 구현하는 방법은 다음과 같다.

var partial = function () {
    var originalPartialArgs = arguments;
    var func = originalPartialArgs[0];
    if (typeof func !== 'function') {
        throw new Error('첫 번째 인자가 함수가 아닙니다.');
    }

    return function () {
        var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
        var restArgs = Array.prototype.slice.call(arguments);
        return func.apply(this, partialArgs.concat(restArgs));
    };
};

var add = function () {
    var result = 0;
    for (var i = 0; i < arguments.length; i++) {
        result += arguments[i];
    }
    return result;
};

var addPartial = partial(add, 1, 2, 3, 4, 5);

partial 함수는 첫 번째 인자가 함수인지 검사하고
함수가 맞다면 두 번째 이후부터의 인자들을 부분 적용시킨다.

두 번째 이후부터의 인자들은 다음의 코드로 얻을 수 있다.

Array.prototype.slice.call(originalPartialArgs, 1);

유사 배열 객체인 originalPartialArgs을 1번째부터 슬라이싱하는 작업이다.
이때 originalPartialArgs은 외부 함수의 변수이며 리턴 함수 내부에서 사용되었다.
따라서 부분 적용 함수를 구현하기 위해 클로저를 활용했다고 볼 수 있다!

④ 커링 함수

커링 함수란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서
순차적으로 호출될 수 있게 체인 형태로 구성한 것이다.

커링은 한 번에 하나의 인자만 전달하며
중간 과정 상의 함수를 실행한 결과는 그 다음 인자를 받기 위해 대기만 할 뿐,
마지막 인자가 전달되기 전가지는 원본 함수가 실행되지 않는다.

아래는 커링 함수를 구현한 예시이다.

var curry3 = function (func) {
    return function (a) {
        return function (b) {
            return func(a, b);
        };
    };
};

var getMaxWith10 = curry3(Math.max)(10);
console.log(getMaxWith10(8)); // 10
console.log(getMaxWith10(25)); // 25

가장 안쪽 리턴값인 func 함수는 2개의 인자를 필요로 한다.
이 2개의 인자가 완전히 전달되기 전까지 원본 함수를 실행되지 않는다!

커링 함수는 화살표 함수로 구현하면 더 직관적으로 표현할 수 있다.

var curry = func => a => b => c => d => e => func(a, b, c, d, e);
profile
읽기 쉬운 코드와 글을 작성해요 📝

0개의 댓글