[JavaScript] 클로저(Closure)

Joowon Jang·2024년 7월 23일

JavaScript

목록 보기
7/17

실행 컨텍스트와 가비지 콜렉터

https://velog.io/@juwon98/JavaScript-execution-context
실행 컨텍스트에 대해 모른다면 위의 글을 먼저 보는 것을 추천함

function counter(time) {
  let count = 0;
  
  for(let i=0; i<time; i++) {
  	count++;
  }
  
  console.log(count);
}

counter(3); // 3

위와 같은 코드가 있다면, counter 함수 실행이 끝났을 때, 안에 있는 count 변수는 어떻게 될까?
함수의 실행 컨텍스트가 종료되어 콜스택에서 사라지면서, 그 함수의 환경(변수 등)도 메모리에서 사라질 것이다.

이렇게 메모리에서 쓸모없어진 것들은 어떻게 처리되는 걸까?

가비지 콜렉터

자바스크립트는 도달 가능성(reachability) 이라는 개념을 사용해 메모리 관리를 수행한다.
‘도달 가능한(reachable)’ 값은 쉽게 말해 어떻게든 접근하거나 사용할 수 있는 값을 의미한다. 도달 가능한 값은 메모리에서 삭제되지 않는다.

  1. 아래의 값들은 그 태생부터 도달 가능하기 때문에, 명백한 이유 없이는 삭제되지 않는다.
  • 현재 함수의 지역 변수와 매개변수
  • 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수 (이 글에서 설명할 클로저)
  • 전역 변수
  • 기타 등등

이런 값은 루트(root) 라고 부른다.

  1. 루트가 참조하는 값이나 체이닝으로 루트에서 참조할 수 있는 값은 도달 가능한 값이 된다.

이 외에 도달할 수 없는 값들은 자바스크립트 엔진에 있는 가비지 콜렉터라고 하는 메모리 관리자에 의해 삭제된다.


클로저 (Closure)

function outer() {
  let count = 0;
  
  return function inner(times) {
    for(let i=0; i<times; i++) {
      count++;
    }
    
    console.log(count);
  }
  
}

const counter = outer();
counter(2); // 2
counter(3); // 5

일단, 위의 함수에는 "클로저"가 존재한다.
클로저를 한 문장으로 정의하면 아래와 같이 말할 수 있다.

어떤 함수 A에서 선언한 변수 a참조하는 내부함수 B외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상

정재남 - '코어 자바스크립트' 中

(개인적으로 여러 문서나 책에서 설명하는 클로저의 개념 중 이 표현이 가장 정확하다고 생각한다.)

결론부터 말하면, 클로저의 변수는 가비지 콜렉터의 수집 대상이 되지 않기 때문에, 함수(의 실행 컨텍스트)가 종료된 후에도 함수 내부의 변수가 기억되어, 계속 사용할 수 있다.

코드가 동작하는 과정을 보면
먼저, outerinner 함수는 아래와 같이 동작한다.

  1. inner 함수에서 사용하는 count라는 변수가 inner 함수에 존재하지 않는다.
  2. 스코프 체이닝을 통해 outer 함수의 count를 사용하게 된다.

코드가 실행되면 counter 라는 식별자에 outer 함수의 반환 값을 저장한다.

  1. outer 함수가 실행되면서 반환 값인 inner 함수를 반환하고, 반환된 inner 함수는 counter라는 식별자를 가지게 된다.
  2. counter(2)를 실행하면 inner 함수가 실행된다. -> count 값이 2가 되어 2 출력
  3. counter(3)를 실행하면 inner 함수가 실행된다. -> 4번에서 실행된 결과로 count 값이 메모리에서 사라지지 않고 유지되어 2이므로 count는 5가 되어 5를 출력

outer 함수의 실행이 종료되었지만, 현재 실행 컨텍스트에서 outer 함수의 count 값을 사용(도달)하고 있기 때문에 가비지 콜렉터의 수집대상이 되지 않는 것이다.

클로저가 필요한 상황

클로저를 어떤 상황에 사용할 수 있을까?
JavaScript로, 그리고 React를 사용하여 여러 기능을 구현하면서 한 가지 공통적으로 클로저가 필요한 상황을 발견했다.
바로, 함수를 직접 실행하지 않고 함수 본문을 참조하는 상황이다.

예를들어, 아래와 같은 코드가 있다.

function handleClick() {
  console.log('clicked');
}

button.addEventListener('click', handleClick);

현재 버튼의 click 이벤트에 handleClick이라는 함수가 실행되도록 작성되어 있고,
handleClick 함수가 실행되면 콘솔에 'clicked'라는 문구가 출력된다.

그런데, 만약 버튼이 클릭될 때 실행되는 함수에서 문구를 몇 번 출력할지를 매 번 다르게 하고싶다면?
물론, 외부에 변수를 설정하고 handleClick 함수에서는 스코프 체이닝을 통해 사용할 수도 있다.
하지만, 함수의 재사용성을 높이고 가독성과 유지보수성을 높인다거나 여러 이유로 좋지 않은 방법이라고 생각한다.

그래서 이런 상황에 클로저를 사용할 수 있다.

function returnHandleClick(times) {
  
  return function handleClick() {
    for(let i=0; i<times; i++) {
      console.log('clicked');
    }
  }
  
}


button.addEventListener('click', returnHandleClick(3));

클로저를 사용한 코드를 살펴보면, 다음과 같이 동작한다.

  1. button의 'click' 이벤트에 returnHandleClick(3)을 실행한 결과가 바인딩된다.
  2. returnHandleClick(3)은 실행 결과로 handleClick 함수를 반환한다.
    2.1. 이 때, returnHandleClick의 인수로 전달된 times 값인 3이 handleClick함수의 times로 사용된다.
  3. button을 클릭하면 'clicked'라는 문구가 3번 출력된다.

지금 예시에서는 returnHandleClick() 대신에 () => handleClick(3) 이런 식으로 사용해도 동일하다고 볼 수 있지만, React를 사용할 때 useCallback을 사용해서 하위 컴포넌트에 전달 할 함수를 메모이제이션 할 수도 있고, 활용할 수 있는 부분이 정말 많다.
특히나, React에서 JSX 마크업의 onClick 등의 속성에 전달할 함수가 길어지는 상황에 인라인 형식을 사용하지 않고, 함수를 바깥으로 분리하기에도 용이하고, forEach문 등에서 index와 같은 인수를 전달할 수 있는 장치가 된다.

profile
깊이 공부하는 웹개발자

0개의 댓글