closure

악음·2021년 11월 26일
0

js

목록 보기
1/5
post-thumbnail

클로저를 공부하게된 이유

면접볼때 쌉소리를 해쌌다.
그리고 면접 복기 하기위해 클로저를 공부하면서 내가 더욱더 말도안되는 쌉소리를 했다는것을 알게되었다
그래서 포스팅한다

참고한 사이트
https://hyunseob.github.io/2016/08/30/javascript-closure/
https://www.bangseongbeom.com/javascript-var-let.html

클로저를 공부안한 이유

그냥 내부에서 외부변수 쓸수있는건 당연한거아님?
이라는 단순하고도 깊이없는 생각과 그냥 클로저의 설명을 들어보면 뭔 소린지 몰라서 파고들기 귀찮다는 개발자로서 하면 안되는 생각을 했다.
스스로의 잘못을 알고있지만 편함많을 찾은 댓가로 면접에서 쌉소리를 했기때문에 광탈각!
(아직모른다 떨어진것은..)
스스로 부끄러움을 알기에 다시 공부해서 포스팅한다.

클로저란

사전적 의미

closure
명사
1.(공장·학교·병원 등의 영구적인) 폐쇄(되는 상황)
factory closures
공장 폐쇄 (사례들)
2.(도로·교량의 일시적) 폐쇄

내가 프로그래밍을 이해하는 방법중 가장 첫번째 방법은 단어가 주는 늬앙스를 이해하는것이다.
언어를 개발하는 사람들은 각자의 철학을 갖고 언어를 만들었다고 생각한다.
때문에 프로그래밍에 사용된 단어들을 먼저 살펴보고 이해하려고 노력한다
이번 포스팅에서는 왜 이기능을 closure라고 지었는지 나름대로의 추측을 하기위헤 closure를 파해쳐 보려고한다.

MDN에 나온 의미는

클로저는 독립적인 변수를 가리키는 함수이다 또한 클로저 안에 정의된 함수는 자신이 만들어진 '환경'을 기억한다(각자 클로저가 생긴 환경의 상황을 메모리에 올린다)

라고 나와있고 쉬운 말로 하면

함수 내에서 함수를 사용하면 클로저라고 한다

함수내에서 함수를 사용한다 라고 하면 react.js 개발자라면 떠오르는 것이 있을 것이다
바로 함수형component이다 함수로 선언된 콤포넌트안에 여러가지 함수를 선언하거나 setState를 자식 component에 props으로 주어 자식 컴포넌트에서 사용할수 있도록하는 방식을 떠올릴것이다.

프론트엔드 개발자가 아니더라도 콜백함수를 살펴보자면 함수를 인자값으로 받아 함수 내부에서 실행시키는 것을 보자면 이것또한 클로저라고 할수도있다.(외부에 환경을 들고 콜백을 받는 함수안에서 실행가는한 부분)

또한 js에선 변수에 함수를 할당하여 인자값으로 넘겨주는 방법도 생각할수있다.

이러한 기능들을 js가 closure를 지원하기 때문이다.

지원하지 않는다고하면 음......클래스 컴포넌트를 사용해야할것이다 ..ㅠㅠ

코드를 통해 closure가 무엇인지 살펴보자

function getClosure(){
	var text ='variable 1'
        return function(){
          return text;
        }
}
// return 된 익명함수(클로저)가 변수에 할당된다. 이때 클로저가 기억하는 외부 환경이 
// 메모리에 올라간다.
var closure =getClosure()
closure()// 'variable 1'
// 이후 클로저가 참조한 환경은 GC가 회수하지 않기 때문에 null을 할당한다.
//(메모리에서 제거)
closure=null

위 코드를 보면 getClosure 함수안에 익명함수가 리턴하고있다.
그리고 결과는 getClosure함수가 return되어 끝났는데도 익명함수에선 정확한 값을 기억하고 있다.
여기서 반환된 익명함수를 클로저라고한다.
MDN에서 말한 것처럼 자기가 호출될때에 환경을 기억하고있다.

다른코드를 살펴보자

반복문을 통한 클로저 사용하기

// 포문예제 1
for (var i = 0; i < 10; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}

위 코드를 실행시켜보면 0.1초뒤에 10만 10번 출력이된다
(i가 let이면 순차적으로 실행된다 호이스팅이 가능한 변수와 그렇지 않은 변수에 따라 작동하는 방식이 다르다.)

정확한 이유는

함수는 함수가 정의될때에 변수 자체만(자기가 가져와야할 변수'만'을 기억한다 '값'은 꺼내오지 않는다)
기억하기 때문이다.

i는 var으로 선언 되었으므로 익명함수에는 하나의 i만을 가리키고있다.

반면 let은 var와는 새로운 작용을한다 (이는 let이 for문의 초기식안에 있을때만 작용한다)
for 루프가 돌때마다 새로운 i를 만들고 여기에 이전 i의 값을 대입하기 때문
즉 함수내부에선 하나의 i가 아닌 매번 새로운 i가 대입된다는것 때문에 각각의 익명함수는 저마다 다른 i를 가르킬 것이기 때문
(만약 let이 for문의 초기식 밖에 존재할경우 이러한 일은 일어나지 않는다)

var functions = [];

let i = 0; // for 문 바깥에서 let 사용
for (; i < 3; i++) { // 초기식을 비워 둠
  functions.push(function () {
    console.log(i);
  });
}

functions[0](); // 출력: 3
functions[1](); // 출력: 3
functions[2](); // 출력: 3

다시 본론으로 돌아와서
포문예제 1의 코드에서 순차적으로 0,1,2,3,4,5.. 형식으로 출력하려면 클로저를 사용하면된다.

for (var i = 0; i < 10; i++) {
  (function (num) {
    // 즉시실행 함수를 사용하여 파라미터를 받아 사용한다.
    // 파라미터는 클로저가 기억하는 환경에 포함되므로 순차적으로 실행이된다.
    setTimeout(() => {
      console.log(num);
    }, 100);
  })(i);
}

그냥 파라미터없이 직접 변수를 할당하면 될 것 같지만 그렇지않다
파라미터로 넘기지 않으면 클로저 가 기억할 환경에는 i가없으니 바로 글로벌 스코프에 있는 변수를 i 하나만을 탐색하여 할당하기 때문이다.

클로저를 왜 closure이라고 지었을까?

클로저가 나온 이유는 자바스크립트가 class가 나오기이전(es6) 객체를 캡슐화 은닉화 하기 위해 만들어졌다 때문에 closure(폐쇠)라는 이름과 관련이 있다.

다음예제를 봐보자

function Counter() {
  this._count = 0;
}
Counter.prototype.counter = function () {
  return (this._count += 1);
};

let counter1 = new Counter();
let counter2 = new Counter();
console.log(counter1.counter()); //1
console.log(counter2.counter()); //1
console.log(counter1.counter()); //2
console.log(counter1._count);    //2

위에 예제는 일반적인 객체의 모습이다
문제는 마지막 코드에있다

counter1._count

객체를 통해 변수에 접근이 가능하다는접이다 이런점때문에 js에서는 캡슐화/은닉화를 위해 클로저를 만들었다
다음은 클로저를 이용한 은닉화한 예제이다.

let getClosure = (function () {
  var _count = 0;
  return function () {
    return (_count += 1);
  };
})();
console.log(getClosure._count); //undefined
console.log(getClosure()); // 1
console.log(getClosure()); // 2
console.log(getClosure._count); //undefined
getClosure= null // 클로저 제거

처음코드와 다른점은 완벽히 은닉화가 되었다는 점이다.

다음은 여러게의 카운터를 만드는 예제이다 즉시실행함수에서 기명함수화 시키면된다.

function CounterFactory() {
  var _count = 0;
  return function () {
    _count += 1;
    return _count;
  };
}
let counter1 = CounterFactory();
let counter2 = CounterFactory();
console.log(counter1()); //1
console.log(counter1()); //2
console.log(counter2()); //1
console.log(counter1._count); //undefined

추가로 객체와 클로저를 이용해서 여러가지 함수를 구현할수도있다.

function counterFactory2() {
        var _count = 0;

        function count(value) {
            _count = value || _count;

            return _count;
        }

        return {
            count: count,
            inc: function() {
                return count(count() + 1);
            },
            dec: function() {
                return count(count() - 1);
            }
        };
    }

    var counter = counterFactory2();
    // 초기값을 설정할 수 있다.
    counter.count(3);
    console.log(counter.inc());
    console.log(counter.inc());
    console.log(counter.dec());

위에 코드처럼 초기화와 객채로 넘겨준 펑션을 사용할 수 있다.

왜 은닉화를 해야하는지?

만약 자판기가 있다고 가정해보자
이 자판기를 객체화 시킨다면
특정 커피의 변수가 있을것이고 남은 갯수가 있을것이다

let blackCoffee = 10

그리고 이 커피를 뽑아먹는 펑션을 구현한다면 다음과 같을것이다

function getBlackCoffee(coin){
  if(coin>=300){
    blackCoffee -=1
  }
  return {
    returnCoin:coin-300,
    blackCoffee:1
  }
}
자판기.getBlackCoffee(300) // {returnCoin:0,blackCoffee:1}

그리고 사용자는 위에 펑션을 사용하여 블랙커피를 사먹어야할것이다
하지만여기서 변수

let blackCoffee

가 은닉화가 안되어있다면 다음과같이 사용할수 있을것이다

자판기.blackCoffee=-100
자판기.blackCoffee // -100

이런식으로 현실적으로 약속되어지지 않은 로직이 만들어 질 것이다.

마무리

지금까지 closure가 왜 closure으로 이름 지어 졌는지 알아보았다.
클로저는 자기가 호출된 환경을 기억하고있고 이를통해 데이터의 캡슐화를 할수 있기에 클로저라고 이름지어졌다.

profile
RN/react.js개발자이며 배운것들을 제가 보기위해서 정리하기 때문에 비속어 오타가 있을수있습니다.

2개의 댓글

comment-user-thumbnail
2021년 11월 27일

잘읽고가요 화이팅!!

1개의 답글