TIL

[JavaScript] 클로저(Closure)

기절초뿡 2023. 3. 8. 20:21

이번 글에서는 JavaScript의 클로저(closure)에 대해서 알아보고, 자바스트립트 라이브러리인 리액트에서의 클로저 활용에 대해 정리해보았습니다. 

 

"A closure is the combination of a function and the lexical environment within which that function was declared."
클로저는 함수와 그 함수가 선언되었을 때의 렉시컬 환경(lexical environment)의 조합이다. 

출처 : MDN

 

클로저(Closure)란?

클로저는 자바스크립트에만 국한되지 않고, 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 특성이다. 

 

🔆 일급 객체(First Class Object) 인 함수 : 함수를 변수에 할당할 수 있고, 함수를 인자로 전달할 수 있으며, 다른 함수의 결과로서 함수가 리턴될 수 있다. 따라서 고차함수를 만들 수 있고, 콜백을 사용할 수 있다. 

 

function outerFunc() {
  var x = 10;
  var innerFunc = function () { console.log(x); };
  return innerFunc;
}

/**
 *  함수 outerFunc를 호출하면 내부 함수 innerFunc가 반환된다.
 *  그리고 함수 outerFunc의 실행 컨텍스트는 소멸한다.
 */
var inner = outerFunc();
inner(); // 10

함수가 return 을 했다는 것은 그 함수의 life cycle이 종료되었다는 의미한다. 즉, outerFunc는 콜스택(실행 컨텍스트)에서 제거될 것이고, outerFunc의 지역변수인 x 또한 더이상 유효하지 않게 될 것이다. 그러나 위 코드를 실행한 결과는 변수 x의 값인 10이다. 이는 내부 함수인 innerFunc가 외부 함수인 outerFunc의 지역 변수에 여전히 접근할 수 있기 때문이다. 

이처럼 자신(내부함수)을 포함하고 있는 외부함수보다 내부함수인 자신이 더 오래 유지되는 경우, 외부함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는데 이러한 함수를 클로저(closure)라고 한다. 

 

즉, 클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수를 말한다. 이를 조금 더 간단히 말하면 클로저는 자신이 생성될 때의 환경(Lexical environment)을 기억하는 함수다라고 말할 수 있다. 

 

클로저 활용 - 전역 변수 사용 억제

let count = 0
function increase(){
  count++
  console.log(`current counter is ${count}`)
  return count
}

increase()
increase()
increase()

increase 함수를 실행할 때마다 변수 count의 값이 1씩 증가하는 코드이다. 이 코드의 문제점은 전역변수를 사용하고 있다는 점이다. 전역변수를 사용하면 의도치 않게 값이 변경될 수 있고, 이는 오류로 이어질 수 있기 때문에 전역변수의 사용은 최소화 하여야 한다. 이때 클로저를 사용해서 변수 count를 지역 변수로 바꿀 수 있다. 

let increase = (function(){
  let count = 0
  return function(){
    count++
    console.log(`current counter is ${count}`)
    return count
  }
})()

increase()
increase()
increase()

즉시실행함수(Immediately-invoked Function Expressions, IIFE)를 사용하여, count를 지역변수로 만들어주었다. 스크립트가 실행되면서 즉시실행함수가 호출되고, 변수 increase에는 return 된 함수가 할당된다. 즉시실행함수는 호출된 이후 소멸되지만, 즉시실행함수로부터 return 된 함수는 변수 increase에 담겨 호출될 수 있다. 이때 클로저인 이 함수는(return 된 함수) 자신이 선언되었을 때의 렉시컬 환경인 즉시실행함수의 스코프에 속한 지역변수 count를 기억한다. 따라서 즉시실행함수의 지역변수인 count에 접근할 수 있고, 변수 count는 자신을 참조하는 함수가 소멸될 때까지 유지된다. 

즉시실행함수는 한 번만 실행되므로 increase가 호출될 때마다 변수 count가 초기화되지 않는다.

 

 

리액트에서의 클로저 

리액트 훅에서도 클로저 개념을 사용하고 있다. 

import { useEffect, useState } from 'react';

export default function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setInterval(() => {
      setCount(count + 1);
    }, 1500);
  }, []);

  useEffect(() => {
    setInterval(() => {
      console.log(count);
    }, 1500);
  }, []);

  return <div>Hello world</div>;
}

setInterval 함수로 1500ms 마다 count의 값을 1씩 증가시키고, 동시에 count의 최신 값을 콘솔에 출력하도록 코드를 작성하였다. 우리는 0, 1, 2, 3, ••• 가 출력되기를 기대하겠지만, 기대와 달리 0, 0, 0, ••• 이 출력된다. 

 

react의 hook 중에서 dependency array를 전달 받는 경우가 있는데, 이 경우 전달받은 dependency array 의 값들이 변경될 경우, 전달 받은 콜백이 다시 실행된다. 즉, 현재처럼 dependency array로 빈 배열을 전달한 경우, mount 될 때 딱 한 번만 실행이 된다는 뜻이다. 

 

현재의 최신 값이 아니라, 콜백함수가 실행되는 시점의 값을 기억한다. 그래서 최신값을 참조하기 위해서는 콜백을 다시 실행해 주어야 하고, 따라서 최신값을 dependency array에 추가해주어야 한다. 

 

 

참고

https://poiemaweb.com/js-closure

https://betterprogramming.pub/understanding-the-closure-trap-of-react-hooks-6c560c408cde

https://betterprogramming.pub/10-javascript-closure-challenges-explained-with-diagrams-c964110805e7