본문 바로가기
개발 공부 일지/코어 자바스크립트 (정재남)

[코어 자바스크립트] 클로저 활용 사례

by yelimu 2025. 3. 4.

클로저는 다양한 곳에서 광범위하게 활용된다.

'외부 데이터' 흐름에 주목하며 살펴보기

클로저 활용 사례

① 콜백함수 내부에서 외부 데이터 사용
② 정보 은닉
③ 부분 적용 함수
④ 커링 함수

① 콜백함수 내부에서 외부 데이터 사용

- 대표적인 콜백함수 : 이벤트 리스너

[ver.1]

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

fruits.forEach(function(fruit){            ------ (A) 외부함수
    var $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', function(){  --- (B) 내부함수
        alert('your choice is ' + fruit);
    })    
    $ul.appendChild($li);
})

document.body.appendChile($ul);

💭fruits 배열의 요소 fruit 개수 만큼 (A) 함수 실행, 그때마다 새로운 실행 컨텍스트 생성

(B) 는 클릭 이벤트에 의해 실행되며 그때마다 각 컨텍스트 생성한다.

 -  A의 L.E (LexicalEnvironment) 참조한다.

‼️A 컨텍스트 종료되어도 B함수가 변수 fruit를 참조할 예정이므로 GC(가비지 컬렉터)의 수거대상에서 제외된다.


[ver.2] 이벤트 리스너 (B)함수를 별도로 분리하면 alertFruit() 로 직접 실행할 수 있다.

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

var alertFruitBuilder = function(fruit) {
    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);
alertFruit(fruits[1]);

⚠️ 그런데 이때 각 li를 클릭하면 과일이름이 아닌 [object MouseEvent]라는 값이 출력된다.

콜백함수 alertFruit 인자에 대한 제어권을 addEventListener가 가지는데, addEventListener 는 콜백함수를 호출할 때 첫번째 인자로 이벤트 객체를 주입하기 때문이다. 

 

=> bind 메서드로 해결 🔨

[ver.3]

    $li.addEventListener('click', alertFruitBuilder.bind(null, fruit));

⚠️ this 가 원래의 this와 다른 대상이 됨

 

=> 고차함수로 해결 🔨 

[ver.4]

...
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);
})

alertFruitBuilder(fruit) : 함수를 실행하면서 fruit를 인자로 전달

alertFruitBuilder 함수의 실행 결과로 반환된 함수가 이벤트 리스너로 전달됨 

 

클릭 이벤트 발생 시 이때 반환된 함수의 실행 컨텍스트가 활성화되면서 alertFruitBuilder 의 L.E를 참조하여 fruit에 접근


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

어떤 모듈의 내부 로직에 대한 외부로의 노출을 최소화 ➡️ 모듈간의 결합도를 낮추고 유연성을 높임

 - 접근 권한 제어 : public / private / protected

 

클로저를 활용하여 외부 스코프에서 함수 내부의 변수들 중 선택적으로 일부의 변수에 대한 접근권한 부여

- return 을 활용 

function A(){
	var a = 10;
	function B (){
            var b = 10;
            return ++a;
    	}
    return B;
    }
var A2 = A();
console.log(A2()); // 2
console.log(A2()); // 3

외부 공간에 노출되어 있는 A라는 변수를 통해 A 함수를 실행할 수 있지만

‼️ A 함수 내부에는 어떠한 개입도 할 수 없음

‼️ A 함수가 return한 정보에만 접근 가능


(자동차 경주 게임 - 접근 권한 제어해보기)

[ver.1] 

var car = {
    fuel : Math.ceil(Math.random() * 10 + 10),
    power : Math.ceil(Math.random() * 3 + 2),
    moved : 0,
    run : function() {
        var km = Math.ceil(Math.random()*6);
        var wasteFuel = km / this.power;
        this.fuel = this.fuel - wasteFuel;
        this.moved = this.moved + km;
        console.log(km + 'km 이동 (총 ' + this.moved + 'km)');        
    }
}

⚠️ car 객체를 정의했는데 이때 프로퍼티에 접근이 가능하다

var.fuel = 99999;
car.power = 999;
car.moved = 9999;

 

값을 바꾸지 못하도록 제한하기  🔨 

-> 객체가 아닌 함수로 만들고, 필요한 멤버만 return

=> 클로저로 변수를 보호, return 한 정보만 공개

[ver.2] 

var createCar = function(){
    var fuel = Math.ceil(Math.random() * 10 + 10)
    var power = Math.ceil(Math.random() * 3 + 2)
    var moved = 0
    return {
        get moved(){
            return moved;
        };
        run : function() {
            var km = Math.ceil(Math.random()*6);
            var wasteFuel = km / power;
            fuel = fuel - wasteFuel;
            moved = moved + km;
            console.log(km + 'km 이동 (총 ' + moved + 'km)');        
        }
    }
}

var car = createCar();

createCar() 함수 호출한 결과를 car 변수에 할당


getter : 프로퍼티 값에 접근
setter : 프로퍼티 값 설정
➤ setter 없이 getter 만 쓰면 읽기전용이 되며, 값 할당 외에 값 수정이 안된다.

ES6 이후 객체 리터럴 안에서 속성 이름 앞에 get 또는 set 키워드만 붙여 Getter와 Setter를 정의할 수 있다

 

car 객체는 아래와 같다

{
  moved: getter function,
  run: function
}

=> car.run(), car.moved 에만 접근이 가능하다

car.fuel = 10000;
console.log(car.fuel); // 10000

이렇게 fuel에 접근하는것처럼 보이지만? 

car 객체에 fuel 프로퍼티가 없으니 새로운 프로퍼티가 추가되는 것일 뿐 원래의 car 객체와는 무관하다.

즉 run() 실행 시 기존의 fuel값을 사용한다.

 

⚠️ 기존의 run 메서드에 다른 함수를 덮어씌우는 어뷰징이 가능하다

car.run = function() { console.log("해킹 완료! 이동 무제한!"); };
car.run(); // "해킹 완료! 이동 무제한!"

 

객체를 return 하기 전에 미리 변경할 수 없도록 freez 사용🔨 

[ver.3] 

var createCar = function(){
    var fuel = Math.ceil(Math.random() * 10 + 10);
    var power = Math.ceil(Math.random() * 3 + 2);
    var moved = 0;
    var publicMembers = {
    	get moved(){
            return moved;
        };
        run : function() {
            var km = Math.ceil(Math.random()*6);
            var wasteFuel = km / power;
            fuel = fuel - wasteFuel;
            moved = moved + km;
            console.log(km + 'km 이동 (총 ' + moved + 'km)');        
    	}
    }
    Object.freeze(publicMembers);
    return publicMembers;
}

Object.freeze(대상 객체) 로 동결된 객체는 더이상 변경될 수 없다.


③ 부분 적용 함수

n 개의 인자를 받는 함수에 미리 m 개의 인자만 넘겨, n - m 개의 인자로 호출하는 함수

[ver.1] 

var add = function() {
    var result = 0;
    for (var i = 0; i < arguments.length; i++) {
        result += arguments[i]
    }
    return result;
}
var addPartial = add.bind(null, 1, 2, 3, 4, 5); // this : null 바인딩
console.log(addPartial(6, 7, 8, 9, 10)); // 55
arguments : 함수가 전달받은 전체 인자를 반환하는 유사 배열 객체, 함수 내부에서만 접근이 가능하다
유사 배열 객체 : 정수 인덱스와 length 속성을 갖는다. 배열같지만 실제 배열이 아님 - 대부분의 배열 메서드 사용 X 

 

add.bind(null,args) : args를 bind를 호출한 add 함수에 전달하여 호출

⚠️ 기존의 this 값을 변경하므로 메서드에서는 사용이 불가하다 

=> this에 관여하지 않도록 좀더 범용적으로 수정하기 🔨 


[ver.2] 

var partial = function() {
    var originalPartialArgs = arguments;
    var func = originalPartialArgs[0]; // 첫번째 인자로 전달된 함수 func
    if (typeof func !== "function") {
        throw new Error("첫번째 인자가 함수가 아닙니다");
    }
    return function() {
    	//(A) partial함수 호출 시 전달받은 인자(유사배열객체).slice(1) 적용 
        var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
        
        //(B) partial 호출 결과 반환한 함수에 전달받은 인자 전체를 얕은 복사
        var restArgs = Array.prototype.slice.call(arguments);
        
        //(A), (B) 인자를 모아서 func에 전달
        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 partial_add = partial(add, 1, 2, 3, 4, 5); // func = add함수
console.log(partial_add(6, 7, 8, 9, 10));
// >> 55

⚠️ 부분적용 함수에 적용할 인자를 앞에서부터 차례로 전달해야만 한다

=> 원하는 위치에 미리 넣어놓고 빈자리에 인자를 채워넣도록 🔨 

 

[ver.3] 

Object.defineProperty(window, "_", {
	value: "EMPTY_SPACE",
    writable: false, 
    configurable: false,  
    enumerable : false
});

var partial2 = function() {
    var originalPartialArgs = arguments;
    var func = originalPartialArgs[0]; // 첫번째 인자로 전달된 함수 func
    if (typeof func !== "function") {
        throw new Error("첫번째 인자가 함수가 아닙니다");
    }
    return function() {
        var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
        var restArgs = Array.prototype.slice.call(arguments);
		for (var i = 0; i < partialArtgs.length; i++){
        	if(partialAtgs[i] === _){
            	partialArgs[i] === restArgs.shift(); // _를 만나면 앞쪽부터 전달
            }
        }
        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 partial_add = partial2(add, 1, 2, _, 4, 5, _, _, 8, 9);
console.log(partial_add(3, 6, 7, 10));
// >> 55

var dog = {
	name: "강아지",
    greet : partial2(function(prefix, suffix){
    	return prefix + this.name + suffix;
    }, '왈왈,')
}
dog.greet("배고파요!"); // 왈왈 강아지 배고파요!
Object.defineProperty(대상 객체, "속성 이름", { 정의할 내용 })
정적 메서드는 객체에 새로운 속성을 직접 정의하거나 이미 존재하는 속성을 수정한 후, 해당 객체를 반환

예제에서는 어쩔수없이 전역 공간을 침범했음., ES6에서는 Symbol.for 를 사용하는 것을 권장

 

- 디바운스 함수

동일한 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한번만 처리하여 성능 최적화

var debounce = function(eventName, func, wait){
    var timeoutId = null;
    return function(event){ // addListener 에서 이벤트 핸들러에 event를 전달 
        var self = this
        console.log(eventName, 'evnet발생');
        //기존 setTimeOut된 timeoutId가 있다면 초기화시킴
        clearTimeout(timeoutId)        
        //새롭게 timeoutId를 설정해줌
        timeoutId = setTimeout(func.bind(self, event), wait) 
        // 인자로 전달받은 이벤트 핸들러 func 에 event 를 인자로 전달
    }
}

var moveHandler = function(e){console.log("move event 처리")};

//마우스 move 이벤트 발생할때마다 기존 timeout을 초기화시키고 다시 setTimeout을 실행시킴
document.body.addEventListener("mousemove", debounce("move", moveHandler, 500))

클로저로 처리되는 변수 : eventName, timeoutId, func, wait


드뎌 마지막, , 

④ 커링 함수

여러개의 인자를 받는 함수를 쪼개서 ⑴ 하나의 인자만을 받아 ⑵ 순차적으로 호출되도록 체인 형태로 구성하는 함수

부분 적용 함수와 비슷해보이지만, 한 개의 인자만 전달받고, 마지막 인자가 전달되기 전까지 원본 함수를 실행하지 않는다.

=> 지연 실행

var curring = function(func){
    return function(a){
        return function(b){
            return func(a, b); // 두개의 인수를 받는 함수 func를 리턴한다
        }
    }
}
var getMaxWith10 = curring(Math.max)(10); // func : Math.max(10, b) 를 getMaxWith10에 할당
console.log(getMaxWith10(8));  //10
console.log(getMaxWith10(25)); //25

 

var curring = function(func){
    return function(a){  
        return function(b){  
            return function(c){
                return func(a, b, c)
            }
        }
    }
}

curring = func => a => b => c => func(a, b, c);

ES6 에서 화살표 함수를 써서 한줄로 표기 

화살표 순서에 따라 함수에 값을 차례로 넘겨주면 마지막에 func가 호출된다는 흐름을 파악하기 쉽다 

 

각 단계에서 받은 인자들을 모두 마지막 단계에서 참조하므로, GC되지 않고 메모리에 차곡차곡 쌓였다가

마지막 호출로 실행 컨텍스트가 종료된 후에야 (더이상 참조하지않으니) 한번에 GC 수거대상이 됨 

 

활용 예) id 값이 선택되었을때 서버 요청을 수행하는 함수 

var getInformation = function (baseUrl){
	return function (path){
    	return function (id){
        	return fetch(baseUrl + path + '/' + id); // 서버에 정보 요청 실행
        }
    }
};

//ES6
var getInformation = baseUrl => path => id => fetch(baseUrl + path + '/' + id);

 


🫠 🫠 🫠

내가 잘 이해한게 맞을까.. 코드를 한줄한줄 짚어가며 최대한 이해하고는 있지만, 어렵다 !!

콜백함수에서 클로저 쓰면서도 쓰고있는지 모르고 있었네 싶다 

드뎌 클로저까지 마쳤고 마지막 프로토타입 한 챕터 남았다 : > 

이 포스팅은 <코어 자바스크립트> 를 읽고 내용을 정리한 포스팅입니다. 틀린 내용이 있다면 알려주세요 _ _