Javascript & React

[Javascript] 자바스크립트 Closure에 대한 이해와 설명( + 예시 )

Hoo_Dev 2023. 1. 25. 14:18

클로저란?

두 개의 함수로 만들어진 환경으로 특별한 객체의 한 종류

-> 외부 함수 호출이 종료되더라도 외부 함수의 지역 변수 및 변수 스코프 객체의 체인 관계를 유지할 수 있는 구조를 클로저라고 한다.

 

++ 클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수

 

https://www.youtube.com/watch?v=LL0DGc5pg7A (라매개발자 - 자바스크립트 클로저 실용적이고 쉬운 설명)

자바스크립트 클로저에 대한 이해를 하기위해 라매개발자님의 유튜브를 시청 후 정리해보았다.

 

우선 상황 하나를 가정하여 생각해보자

1. 어떤 변수 cnt가 있다.

2. cnt라는 값은 무조건 cntPlus로만 바꾸고 싶다.

 

let cnt = 0;
function cntPlus() {
  cnt = cnt + 1;
}

console.log(cnt);
cntPlus();
console.log(cnt);

// 출력
// 0
// 1

위와 같이 cntPlus()를 호출 할 경우 cnt가 1 증가됨을 알 수 있다.

하지만 완벽하지가 않다.

 

만약 이 사이에 1억개의 코드가 있다고 가정하자.

중간에 cnt = 100이라는 코드를 실행시키고, cntPlus를 하게 된다면 101이 출력하게 될 것이다.

 

어느점이 문제인가?

1. 우리는 cntPlus로만 값을 증가시킬 수 있어야만 한다.

2. 하지만 중간에 cnt = 100 과 같이 cnt라는 변수가 접근이 가능하기 때문에 우리가 생각하는 상황 구현이 힘들다.

 

그래서 우리는 cnt에 접근을 못하게 해야만 한다. 

 

그 때 필요한게 클로저(Closure)이다.

 

그 방법은 우선 함수로 한 번 감싸줌으로써 전역변수를 지역변수로 만들어준다.

function closure() {
  let cnt = 0;
  function cntPlus() {
    cnt = cnt + 1;
  }
}

console.log(cnt);
cntPlus();
console.log(cnt);

하지만 위와 같이 작성 할 경우 cntPlus또한 클로저 함수의 지역범위에 있는 함수이기 때문에 참조할 수가 없다. 그러기 위해선 cntPlus 함수를 리턴해주게 되는데

function closure() {
  let cnt = 0;
  function cntPlus() {
    cnt = cnt + 1;
  }

  return {
    cntPlus,
  };
}

const cntClosure = closure();
console.log(cntClosure);
// cntClosure를 출력해보면
// { cntPlus: [Function: cntPlus] } 와 같이 나오게 되는데
// 이는 cntClosure는 객체고, 이 객체에 cntPlus라는 키값을 가지는 cntPlus 함수가 담겨있는 것을 볼 수 있다.
cntClosure.cntPlus();

해당 상황에서 cntClosure안에 있는 cntPlus함수를 실행시킴으로써 closure 함수 안에 있는 지역 변수인 cnt가 증가하는 것을 알 수 있다.

 

하지만, 전역에서 console.log를 출력 해 볼 순 없는데 어떻게 해야 출력이 가능할까?

 

function closure() {
  let cnt = 0;
  function cntPlus() {
    cnt = cnt + 1;
  }
  function printCnt() {
    console.log(cnt);
  }
  return {
    cntPlus,
    printCnt,
  };
}

const cntClosure = closure();
cntClosure.printCnt();
cntClosure.cntPlus();
cntClosure.printCnt();

// 출력
// 0
// 1

위와 같이 closure 함수 내부에 출력하는 함수를 작성 후 똑같이 리턴해주게 된다면 출력 내용을 볼 수 있다.

 

위와 같은 방법으로 작성을 하게 된다면 위에 했던 cnt를 바꿔버린 예시와 같이 cnt를 직접적으로 접근하여 바꿀 수 있는 방법은 존재하지 않는다.

 

또한 추가적인 기능을 작성하고자 하면 closure 함수 내에서 새롭게 함수를 추가 하면 되는데, cnt를 100으로 바꾸는 함수를 작성해보자

 

function closure() {
  let cnt = 0;
  function cntPlus() {
    cnt = cnt + 1;
  }
  function setCnt(value) {
    cnt = value;
  }
  function printCnt() {
    console.log(cnt);
  }
  return {
    cntPlus,
    printCnt,
    setCnt,
  };
}

const cntClosure = closure();
cntClosure.printCnt();
cntClosure.cntPlus();
cntClosure.printCnt();
cntClosure.setCnt(100);
cntClosure.printCnt();

// 출력
// 0
// 1
// 100

클로저의 용도는 위의 예시와 같은 경우에 사용하는 것이다. (react의 useState의 방법이 클로저와 같은 방법이지 않을까 생각한다)

 

클로저의 예시

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, i * 1000);
}

위의 코드를 작성하게 된다면,

i가 0 ~  4까지 1초 간격으로 출력이 될 것으로 예상하기가 쉬운데 

실제로 출력 된 값을 보면 1초 간격으로 5가 5번 출력된다.

 

그 이유를 보자면

 

자바스크립트의 기본 동작은 for문이 먼저 돌고 그 이후에 콜백함수가 실행된다.

그 뜻은 자바스크립트의 엔진에서 for문을 먼저 순회하고, 비동기 콜백함수가 실행되기 때문이다.

 

함수 안의 변수 i 는 콜백함수가 실행 될 때 값이 결정되고, 반복문이 먼저 돌아 i가 5로 바뀌어 있는 setTimeout 함수가 다섯번 찍혀지게 되는 것이다.

 

5가 다섯번 찍히는 내부 동작 예시(즉, i가 5로 결정 된 상태에서 setTimeout 함수가 실행된다.)

setTimeout(function(){
  console.log(i);
}, 0 * 1000);

setTimeout(function(){
  console.log(i);
}, 1 * 1000);

setTimeout(function(){
  console.log(i);
}, 2 * 1000);

setTimeout(function(){
  console.log(i);
}, 3 * 1000);

// ...

 

이를 클로저 방식으로 작성하게 된다면 비동기 함수와 반복문의 설계에서 의도한 대로 동작할 수 있다.

 

for (var i = 0; i < 5; i++) {
  function closure(args) {
    setTimeout(function () {
      console.log(args);
    }, i * 1000);
  }
  closure(i);
}

for문에서 선언한 i를 클로저 함수가 매개변수로 받았고, 이는 콜백함수 이므로 for문의 실행이 끝난 후에도 클로저 함수는 전달받은 i를 초기화 하지 않고 가지고 있게 된다.

 

내부 동작 예시

function closure(args) {
  setTimeout(function() {
    console.log(args);
  }, 0 * 1000);
 }
closure(0);

function closure(args) {
  setTimeout(function() {
    console.log(args);
  }, 1 * 1000);
 }
closure(1);

function closure(args) {
  setTimeout(function() {
    console.log(args);
  }, 2 * 1000);
 }
closure(2);

...

각각의 콜백 함수들은 실행되는 시점의 i 값에 의해 실행되므로 외부 함수의 실행 컨텍스트는 종료가 되더라도 i 값을 참조 복사 하여 가지고 있기 때문에 각각의 i 값으로 출력이 가능하다.