개발공부/자바스크립트

[JavaScript] 스터디 7일차_ 클로저 (2) 활용

햄❤️ 2021. 7. 1. 23:50
반응형

✨ 클로저의 활용 사례

 

1-1-1. 콜백함수 내부에서 외부 데이터를 사용하고자 할 때 _ 내부함수로 선언해서 외부변수 직접 참조(클로저O) 

  - B 콜백 함수는 fruit 이라는 외부 변수를 참조하므로 클로저 생성, B함수가 참조할 예정인 fruit는 A 함수가 종료되어도 GC(가비지컬렉터)의 수거대상에서 제외된다. 

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

fruits.forEach(function (fruit){ //A
	var $li = document.createElement('li');
    $li.innerText = fruit; //fruit을 매개변수로 받아서 사용하기 때문에 외부변수 사용이 아님
    $li.addEventListener('click', function(){ //B
    	alert('your choice is' + fruit); //외부변수인 fruit를 사용하고 있음 
    });
    $ul.appendChild($li);
});
document.body.appendChild($ul);

실행결과

1-1-2 콜백함수 내부에서 외부 데이터를 사용하고자 할 때 _ 공통함수로 빼기

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

//콜백함수를 공통함수로 사용하기 위해 외부로 뺐다.
var alertFruit = function(fruit){
	console.log(fruit) // MouseEvent {...}
	alert('your choice is' + fruit);
};

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

document.body.appendChild($ul);
alertFruit(fruits[1]);

실행결과

 

그러나 list를 클릭하면, [object MouseEvent] 가 출력이 된다. 콜백함수의 인자에 대한 제어권을 addEventListener가 가진 상태이며, addEventListener는 콜백 함수를 호출할 때 첫번째 인자에 "이벤트 객체"를 주입한다. (💔?!!ㅠ) 

 🎀 이벤트 객체 = 해당 타입의 이벤트에 대한 상세 정보를 저장하고 있다. 이벤트 객체는 이벤트 리스너가 호출될 때 인수로 전달된다. 

 

 

1-2. 콜백함수 내부에서 외부 데이터를 사용하고자 할 때_ bind 메서드 사용(클로저x)

 - this를 사용하지 않기 때문에 첫번째 인자에 null 설정, 두번째 인자인 fruit을 매개변수로 바인드된 함수 alertFruit에 넘겨줌

 - 매개변수를 넘겨서 받았기 때문에, 클로저는 생성되지 않았다.

 - 이벤트 객체(fruit)가 인자로 넘어오는 순서가 바뀌는 점 (애플 -> 바나나 -> 피치 -> 바나나) ➡ 바나나
    및 함수 내부에서의 this가 원래 this와 달라진다. ( 전역 ➡ null? )

 - 링크

fruit가 새롭게 반환되는 함수(alertFruit)의 인자로 사용될 것이다. bind를 사용하면 새로운 함수를 반환하고, 새로운 함수는 첫번째 인자로 항상 fruit을 받으며 전역 컨텍스트를 사용한다( null이 컨텍스트로 사용되기 때문에) 

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

//콜백함수를 공통함수로 사용하기 위해 외부로 뺐다.
var alertFruit = function(fruit){
	alert('your choice is' + fruit);
};

fruits.forEach(function(fruit){
	var $li = document.createElement('li');
    $li.innerText = fruit;
    //bind 함수 사용
    $li.addEventListener('click', alertFruit.bind(null, fruit));
    $ul.appendChild($li);
});

document.body.appendChild($ul);
alertFruit(fruits[1]);

실행결과

 

 

1-3. 콜백함수 내부에서 외부 데이터를 사용하고자 할 때_ 콜백함수를 고차함수로 바꿔서 사용하는 방식(클로저O)

고차함수 (Higher-Order Function)

 - 고차함수란 하나 이상의 함수를 인자로 취하거나 함수를 결과로 반환(return) 하는 함수이다. = 함수가 함수를 인자로 받는다
    (=자바스크립트에서 함수는 일급객체이기 때문이다(함수를 값으로 사용할 수 있음) 

 -  고차 함수는 인자로 받은 함수를 필요한 시점에 호출하거나 클로저 생성하여 반환한다.

- 자바스크립트에서 제공하는 고차 함수의 예, map, filter, forEach, sort 등

  🍄 고차함수의 예시 

function calculate(a,b){

    function addValue(){
        return a + b;
    }

    return addValue; // 함수를 리턴!
}

let add = calculate(4,5);
console.log(add()); // 9
function calculate(val, func) { // 함수를 인자로 전달!
    return func(5, val);
}

function addValue(a, b) {
    return a + b;
}

console.log(calculate(7, addValue)); // 12

 

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

 

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

🌟 정보 은닉

어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념 중 하나 = 내부 변수와 메서드를 안전하게 관리할 수 있다.

자바스크립트는 기본적으로 변수 자체에 이러한 접근 권한을 직접 부여하도록 설계되어있지 않다. but 클로저를 이용하여 구분 가능하다.

🌟 접근권한 종류
 

 - public

 - private

 - protected

🌟  private method : 코드에 제한적인 접근만을 허용, 전역 namespace를 관리하는 방법을 제공하여 불필요한 method가 공용 interface를 남용하는 것을 방지

var outer = function(){
	var a = 1;
    var inner = function(){
    	return ++a;
    };
    return inner;
};

var outer2 = outer();
console.log(outer2());
console.log(outer2());

 

outer함수를 종료할 때, inner 함수를 반환함으로써 outer 함수의 지역변수인 a의 값을 외부에서도 읽을 수 있다.
클로저를 활용하면 return을 통해 외부 스코프에서 함수 내부의 변수들 중 선택적으로 변수에 대한 접근 권한 부여 가능

outer함수가 return한 inner 함수에 접근이 가능하다.
외부에 제공하고자 하는 정보(public)를 모아서 return내부에 사용할 정보(private)들은 return하지 않는 것

캡슐화는 간단히 내부의 값들을 외부에서 참조하지 못하도록 숨기는 것으로 클로저의 역할

기존의 javascript의 prototype를 이용해 구현하는 방식이 있다.(this 키워드를 통해 prototype으로 묶음) -> 컨벤션일뿐 외부에서 접근/수정 가능하다. 
 but 클로저를 이용하여 안정적으로 구현 가능하다.

function Count() {
    this._count = 0
}

Count.prototype.counting = function() {
    console.log(++this._count)
}

const counter = new Count();

counter.counting() // 1
counter.counting() // 2

counter._count = 99

counter.counting() // 100

 

//Private 변수로 _ underscore을 사용하는 보통의 네이밍 컨벤션임

function Count() {
    let _count = 0
    return function() {
        console.log(++_count)
    }
}

const counting = Count()
counting() // 1
counting() // 2

counting._count = 100;
counting() //3

 

❓간단하지 않은 자동차 객체 예제

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;
    if(this.fule < wasteFuel){
    	console.log('이동불가');
        return;
    }
    this.fuel -= wasteFuel;
    this.moved += km;
    console.log(km + 'km 이동 (총 '+this.moved +'km)');
    }
};
car.fuel = 10000;
car.power = 100;
car.moved = 1000;


// 로 설정한다면 car.run() 했을때
//2km 이동 (총 1002km)

 

🍄 클로저로 변수를 보호한 자동차 객체(1)

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;
            if(fuel < wasteFuel){
            	console.log('이동불가');
                return;
            }
            fuel -= wasteFuel;
            moved += km;
            console.log(km + 'km 이동 (총 '+moved+'km). 남은 연로: '+fuel);
        }
    };
};

var car = createCar();

car.run();
//4km 이동 (총 4km). 남은 연로: 11

console.log(car.moved); //4
console.log(car.fuel); //undefined
console.log(car.power); //undefined


car.fuel = 1000;
console.log(car.fuel); //1000
car.run(); // 3km 이동 (총 7km). 남은 연로: 10.25 

car.power = 100;
console.log(car.power); //100
car.run(); // 6km 이동 (총 13km). 남은 연로: 8.75

car.moved = 1000;
console.log(car.moved); // 13 
car.run(); // 4km 이동 (총 17km). 남은 연로: 7.75




 

클로저를 활용해 접근권한을 제어하는 방법

 1) 함수에서 지역변수 및 내부함수 등을 생성한다

 2) 외부에 접근권한을 주고자 하는 대상들로 구성된 참조형 데이터(대상이 여럿일 때는 객체 또는 배열, 하나일때는 함수)를 return 한다 
   ✔ return한 변수들은 공개 멤버, 그렇지 않으면 비공개 멤버

 

💎즉시호출함수(IIFE)

= Immediately Invoked Function Expression

(function () {
    //변수 선언
    //변수 처리
    //특정 로직 등 구현
})();

   ✔ 괄호로 둘러쌓인 익명함수

   ✔ 맨 끝에 () 괄호를 통해 함수를 즉각 실행시키는 구조.  즉 선언과 할당 및 실행이 동시에 이루어진다. 

  ✔ 즉시호출 함수를 사용하는 이유??

     즉시호출함수에 정의한 기능,변수들은 호출 시 단 한번만 실행되고 종료된다.
     JS는 변수,함수 선언시 전역으로 선언되기에 즉시호출함수 선언 시 전역에 함수, 변수가 선언되지 않아 충돌이 없다. 

 

 ✔ 즉시 실행 함수의 내부에서 정의된 변수는 외부 범위에서 접근 불가

(function () {
	var crying = '클로저는 어려워';
})();

console.log(crying);
// ReferenceError: crying is not defined
    

   ✔ 즉시 실행 함수를 변수에 할당할 때, 함수가 담기는 것이 아닌 return 값만 담는다.

var result = (function () {
	var crying = '클로저는 어려워';
    return crying;
})();

console.log(result); //클로저는 어려워

 

  ✔ 즉시실행함수와 클로저

  - 즉시실행함수를 이용한 클로저 생성

//f의 리턴값 f2 함수가 count 변수를 참조해 클로저 생성
// f1 함수 내 스코프 계속 유지

const f = (function f1() {
    let count = 0;
    return function f2() {
        console.log(`나는 ${++count}번 호출됨.`);
        return
    }
})();

f();   // 출력값 : "나는 1번 호출됨."
f();   // 출력값 : "나는 2번 호출됨."

 

- 일반 함수표현식을 이용한 클로저 생성

//즉시실행함수가 아닌 일반적인 함수표현식 사용
// 함수가 실행될때마다 count가 0으로 초기화 되기 때문에 결과값은 계속 1이다

const f = function f1() {
    let count = 0;
    return function f2() {
        console.log(`나는 ${++count}번 호출됨.`);
        return
    }
}

f()(); // 출력값 : "나는 1번 호출됨."
f()(); // 출력값 : "나는 1번 호출됨."

 

😣 즉시실행함수인 f1 함수는 선언과 동시에 딱 한번 실행이 되고, 이후 f() 함수를 실행했을 때에는 f2() 함수만 실행이 된다. f2() 함수는 클로저를 만들어 count 변수가 살아있기 때문에 값이 계속 누적된다. 
   = f() 함수만 접근할 수 있는 스코프 생성 



 === 즉시실행함수는 호출 이후 소멸되나, 즉시실행함수가 반환한 함수인 f2()는 자신이 선언되었을 때의 렉시컬 환경인 즉시실행함수 f1()의 스코프에 속해있는 지역변수 count를 기억한다!!! 그래서 즉시실행함수의 변수인 count에 접근할 수 있고, 변수 count는 자신을 참조하는 함수가 소멸할때까지 유지된다. 변수 count는 외부에서 직접 접근할 수 없는 private 변수이므로 안정적이다. 

 

  🍋 즉시실행함수를 이용한 클로저 생성_2

    - 카운터를 생성하는 익명함수를 즉시호출함수로 만들고 그 결과를 counter 변수에 할당한다. 

    - 익명함수의 반환값인 세 함수 increment, decrement, value는 비공개멤버에 접근할 수 있는 클로저이다

var counter = (function() { //IIFE 함수
  var privateCounter = 0; // private member
  function changeBy(val) { // private member
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };
})();

console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1

 

🍋 기명함수를 이용한 클로저 생성_2

 

    - 익명함수를 makeCounter라는 변수에 저장하고, 이 변수를 이용해 여러개의 독립적인 카운터를 만들 수 있다. 

    - 그들 고유의 클로저를 통한 privateCounter 변수의 다른 버전을 참조한다. 카운터가 호출될 때마다, 하나의 클로저에서 변수값을 변경해도 다른 클로저의 값에 영향을 주지 않는다.

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
};

var counter1 = makeCounter();
var counter2 = makeCounter();
alert(counter1.value()); /* 0 */
counter1.increment();
counter1.increment();
alert(counter1.value()); /* 2 */
counter1.decrement();
alert(counter1.value()); /* 1 */
alert(counter2.value()); /* 0 */

 

#클로저를 사용하는 이유는?  

더보기

1. 전역 변수의 사용을 억제하기 위해(전역변수를 대체)

2. 정보를 은닉하기 위해(변수 은닉)
3. 실수를 줄이기 위해

 

 

출처

코어 자바스크립트(정재남, 위키북스)

https://wookgu.tistory.com/7

https://shyunju7.tistory.com/7


https://akasai.space/closure-and-private/

반응형