[Javascript] 클로저 총정리 ① | 클로저의 의미와 IIFE를 활용한 예시

Re_Go·2023년 12월 22일
0

Javascript

목록 보기
25/44
post-thumbnail

1. 클로저란?

클로저는 ECMAScript 사양에 등장하지 않으며, 고유의 개념도 아닙니다. 그러나 MDN에서는 클로저에 대해 "함수와 그 함수가 선언된 렉시컬 환경과의 조합" 이라고 설명하고 있습니다.

이 말이 무슨 뜻이냐면, 실행 컨텍스트에서 해당 함수가 정의 된 시점에서는 해당 함수의 렉시컬 환경의 구성 요소중 하나인 환경 레코드의 [[Enviroment]] 슬롯(정확히 말하면 외부 렉시컬 환경 참조)에 자신이 정의 된 환경인 상위 스코프의 참조를 저장합니다.

그리고 여기서 정리되는 클로저의 개념은 상위 함수(상위 스코프)보다 하위 함수(현재 스코프)가 더 오래 유지되는 경우에, 즉 스택 상에 외부 함수가 아래에 있고 그 위에 중첩 함수가 위치할 경우 중첩 함수에서 외부 함수를 참조하고 있는 동안에는 상위 함수가 종료되어도 상위 함수의 렉시컬 환경을 참조하는 동안은 해당 환경이 사라지지 않고 유지가 되어 접근 및 사용이 가능하다는 것인데, 이러한 개념을 바로 클로저라고 부르는 것이죠.

좀 더 정확히 정리하자면, 스택 상에서 특정 컨텍스트가 제거되어도 그 컨텍스트의 렉시컬 환경을 참조하는 다른 컨텍스트가 존재한다면 가비지 컬렉터는 그 렉시컬 환경을 제거하지 않기 때문에 해당 컨텍스트의 중첩 함수가 환경 레코드에서 [[Environement]] 슬롯에 해당 상위 스코프(상위 함수의 렉시컬 환경)을 참조하고 있는 한 해당 스코프에서 요소들을 검색할 수 있는 개념이 바로 클로저라고 정리할 수 있습니다.

그러나 중첩 함수가 상위 스코프(함수)의 렉시컬 환경에서 어떠한 요소라도 참조하지 않거나, 컨택스트 스택 상 중첩 함수가 먼저 종료되어 외부 함수의 렉시컬 환경을 참조하지 않는 경우에는 클로저라고 보지 않기 때문에, 이러한 클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고, 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 개념으로 보는 것이 일반적입니다.

추가로 클로저는 상위 스코프의 렉시컬 환경을 참조하고 있는 주체를 의미한데, 주로 즉시 실행 함수(IIFE)와

2. 클로저의 활용 예시 - IIFE

IIFE의 조합으로 변수의 상태가 의도치 않게 변경되지 않도록 안전하게 은닉하고, 특정 함수에게만 상태 변경을 허용하기 위해서 사용합니다.

① 즉시 실행 함수를 이용해 특정 변수에 대한 함수 우선권 제공) 현재 전역 변수 num 는 increase 함수 뿐만 아니라 다른 메서드나 함수에서도 접근이 가능한 전역 변수인데, 이 num을 increase 함수로만 값을 참조하고 변경되게 하기 위해서는 num을 함수의 지역 변수로 다시 선언을 해준 후 클로저(함수의 지역 변수 num을 잡고 있는 반환 함수) 함수에 의해서만 increase의 지역 변수 num을 제어할 수 있게 합니다.

let num = 1; // 전역 변수 num의 초기값을 0으로 할당시 모든 메서드에서 접근이 가능합니다.

const increase = (function() {
    let num = 0; // num 변수를 increase 함수의 지역변수로 선언 후
    return function() { // 반환할 함수에 increase의 num 변수를 참조 하고 있는 구문을 포함 시켜 반환해 주도록 합니다. 
        return ++num; // 이 경우 함수가 즉시 실행되어 let num = 0 구문이 한 번 실행되고 해당 변수(num)을 참조하여 증가시키고 반환하는 함수를 increase에 반환하게 되는데, 이후 increase를 다시 호출하면 반환 된 함수만 실행 되므로 let num = 0 구문이 실행되지 않고 반환 된 함수의 코드가 바로 실행되기에 사실상 처음 선언했던 함수의 지역 변수 num의 상태를 유지하고 제어할 수 있는 것이죠.
    };
})();

const anotherIncrease = function() { //아 함수는 지역 변수를 참조하여 값을 리턴하는 변수입니다.
 	return num *= 2; 
};

console.log(increase()); //1 increase 변수를 실행할 때마다 처음 최초 실행된 함수의 지역 변수인 num을 계속 참조할 수 있게 되고 함수가 호출 될 수록 ++num이 계속 실행될 수 있습니다.
console.log(increase()); // 2
console.log(increase()); // 3
console.log(anotherIncrease()); // 2 다른 함수에서 참조하는 num은 increase 함수에서 선언된 지역 변수 num과는 무관하므로 전역 변수 num을 참조하여 코드를 실행하게 됩니다.
console.log(anotherIncrease()); // 4
console.log(anotherIncrease()); // 8


② 즉시 실행 함수와 생성자 함수 반환 및 프로토타입 메서드 클로저를 활용한 예시) 즉시 실행 함수 안에 생성자 함수와 생성자 함수의 프로토타입 메서드를 정의합니다. 이떄 프로토타입 메서드에서 즉시 실행 함수 안에 선언된 지역 변수인 num을 참조하게 됨으로 해당 메서드들이 클로저가 되고, 이후 해당 생성자 함수를 반환 받은 Counter 변수를 이용해 인스턴스들을 생성하게 될 경우 지역 변수 num을 잡고 있는 프로토 타입 메서드들로 독자적으로 접근 및 제어가 가능해집니다.

const Counter = (function(){
    let num = 0; // 해당 프로퍼티가 생성자 함수의 프로퍼티였다면 인스턴스에서도 접근이 가능했으나, 현재 num 프로퍼티는 즉시 실행 함수의 프로퍼티이기에 Counter 생성자 함수의 인스턴스에서는 해당 프로퍼티에 접근하지 못합니다.

    function Counter(){ // 생성자 함수 Counter를 생성합니다.
        // 이 Counter 생성자 함수에는 num을 참조하는 값이 없으나, 프로토타입 메서드인 increase와 decrease에서 num을 클로저로 포획하고 있기 때문에 Counter 객체(this) 에서 num 변수를 가지고 있지 않더라도 num 변수를 유지시키고 사용할 수 있습니다.
    }

    Counter.prototype.increase = function(){ // Counter 생성자 함수의 프로토타입 메서드인 increase를 선언하여 num의 값을 누적 증가시켜 반환하는 메서드를 구현합니다. 이때 increase 메서드는 num 변수를 클로저로 포획하고 있습니다.
        return num++;
    };
    Counter.prototype.decrease = function(){ // decrease 메서드도 increase 메서드 설명과 동일하게 num 변수를 클로저로 포획하여 사용하고 있습니다.
        return num > 0 ? --num : 0;
    };

    return Counter; // 생성자 함수 Counter를 반환하고, 위의 두 프로토타입 메서드 또한 즉시 실행 함수가 정의 될 때 즉시 실행되므로 해당 생성자 함수의 프로토타입 체인에 남아있는 상태로 이후 인스턴스가 해당 메서드를 언제든지 사용 가능합니다.
});

const counter = new Counter(); // Counter 객체 생성

console.log(counter.increase()); // 객체의 increase 메서드 호출시 1 반환
console.log(counter.increase()); // 마찬가지로 2 반환
console.log(counter.decrease()); // 객체의 decrease 메서드 호출시 2에서 1로 감소시키고 반환
console.log(counter.decrease()); // 마찬가지로 0 반환IIFE와 콜백 함수를 클로저로 잡고 있는 익명 함수의 반환을 활용한 예시) 앞서 살펴본 사례와 같이 counter 변수에는 즉시 실행 함수에 의해 한 번만 실행되고, 그 결과인 클로저 함수(익명 함수)에서 참조하고 있는 conter 변수를 콜백 함수에 전달 후 그 콜백 함수의 여부에 따라 반환 된 값을 IIFE 함수의 지역 변수 counter에 할당하여 반환하는 것으로 counter를 증가(increase) 시키거나 감소(decrease)시키고 해당 counter 변수의 상태를 반환하는 코드의 구현이 가능합니다. 

const counter = (function(){ 

    let counter = 0; 
  
    return function(func){ 
        return func(counter); 

    };
}());

function increase(n){ // 증감 함수
    return ++n;
}
function decrease(n){ // 감소 함수
    return --n;
}

console.log(counter(increase));
console.log(counter(increase));
console.log(counter(decrease));
console.log(counter(decrease));


★ 콜백 함수 마다 개별적인 렉시컬 환경을 저장하는 클로저 함수를 반환하는 예시) 콜백 함수를 인자로 받아 고차 함수에 선언된 counter 변수에 해당 콜백 함수로 실행한 결과값을 할당한 후 반환시키는 함수를 반환할 때 고차 함수 makeCounter에서 반환 되는 익명 함수는 자신이 선언된 지점인 makeCounter 고차 함수의 스코프에 속하여 counter 변수를 기억하고 있는 클로저 입니다. 

이 말인 즉 함수가 호출될 때마다 해당 함수는 자신만의 독립된 렉시컬 환경을 갖게 된다는 것인데, 반환되는 각각 함수들의 렉시컬 환경들은 개별적으로 내부 슬롯인 [[Environement]] 슬롯에 곧 소멸될 해당 상위 스코프인 makeCounter 함수의 렉시컬 환경을 기억하게 되기 때문에 사용자가 의도한 대로 각각의 각각의 인스턴스를 실행 할 때 마다 고차 함수(makeCounter)의 하나의 num 변수만 공유하는게 아니라, 개별적으로 생성된 각각의 함수의 렉시컬 환경의 상위 스코프를 참조하여 각각의 num 변수에 접근 및 변경을 할 수 있게 되는 것이죠.

1) function makeCounter(func){
    let counter = 0; 

    return function(){ // 콜백 함수를 호출하여 counter를 조작 후 재할당 하여 반환하는 익명 함수를 반환합니다.
        counter = func(counter); // 이때 반환되는 익명 함수에는 상위 스코프인 makeCounter의 렉시컬 환경을 [[Environment]] 슬롯에 저장해 둡니다.
        return counter;
    };
}

function increase(n){ // 증감 함수
    return ++n;
}
function decrease(n){ // 감소 함수
    return --n;
}

const increaser = makeCounter(increase); // makeCounter 고차 함수에 increaser 콜백 함수를 넘겨주고 반환 된 익명 함수를 increaser에 저장
const decreaser = makeCounter(decrease); // 위 내용과 동일하게 반환 된 익명 함수를 decreaser에 저장

console.log(increaser()); // 1 증가 (1)
console.log(increaser()); // 1 증가 (2)
console.log(decreaser()); // 1 감소 (-1)
console.log(decreaser()); // 1 감소 (-2)
profile
인생은 본인의 삶을 곱씹어보는 R과 타인의 삶을 배워 나아가는 L의 연속이다.

0개의 댓글