클로저는 다양한 곳에서 광범위하게 활용된다.
'외부 데이터' 흐름에 주목하며 살펴보기
클로저 활용 사례
① 콜백함수 내부에서 외부 데이터 사용
② 정보 은닉
③ 부분 적용 함수
④ 커링 함수
① 콜백함수 내부에서 외부 데이터 사용
- 대표적인 콜백함수 : 이벤트 리스너
[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);
🫠 🫠 🫠
내가 잘 이해한게 맞을까.. 코드를 한줄한줄 짚어가며 최대한 이해하고는 있지만, 어렵다 !!
콜백함수에서 클로저 쓰면서도 쓰고있는지 모르고 있었네 싶다
드뎌 클로저까지 마쳤고 마지막 프로토타입 한 챕터 남았다 : >
'개발 공부 일지 > 코어 자바스크립트 (정재남)' 카테고리의 다른 글
[코어 자바스크립트] 클래스 (0) | 2025.03.10 |
---|---|
[코어 자바스크립트] 프로토타입 (0) | 2025.03.10 |
[코어 자바스크립트] 클로저 개념 (0) | 2025.03.04 |
[코어 자바스크립트] 비동기 제어로 콜백 지옥 탈출하기 (1) | 2025.02.25 |
[코어 자바스크립트] 콜백 함수 (0) | 2025.02.25 |