[JavaScript] 클로저(Closures)

/

클로저(Closures)란?

클로저란 함수가 중첩된 상황에서 내부 함수가 자신이 선언된 어휘적 환경을 기억하고, 외부 함수가 이미 반환이 완료되어 소멸한 상황에서도 내부 함수가 외부 함수에 존재하는 변수를 필요로 한다면 그 변수를 계속 사용할 수 있는 것을 의미한다.

작동 방식

function outer() {
  const value = "outer";

  function inner() {
    console.log(value);
  }

  inner();
}

outer();

이 예시의 inner는 자신이 선언된 시점을 기준으로 렉시컬 스코핑이 이루어지기 때문에 상위 스코프인 outer 함수를 탐색하여 value 값을 출력할 수 있다. 즉, 이 코드는 다음과 같은 순서로 작동한다.

  1. outer 함수가 호출된다.
  2. inner 함수가 호출된다.
  3. inner 함수는 지역 스코프 내에서 value를 탐색하고, 존재하지 않아 탐색에 실패한다.
  4. 렉시컬 스코핑에 따라 선언 시점에서 상위 스코프인 outer에서 value를 탐색한다.
  5. value를 발견하였으므로 "outer"를 출력한다.

이 코드는 스코프에 대한 기본적인 이해만으로도 충분히 코드 작동을 예상할 수 있다.

그러나,

function outer() {
  const value = "outer";

  const inner = function () {
    console.log(value);
  };

  return inner;
}

const getInner = outer();
getInner();

이 예시의 경우 outer 함수는 호출되어 inner 함수를 반환하고, 이를 getInner 변수에 할당함에 따라 역할을 마치고 소멸했다. 즉, getInner에는

function() {console.log(value)}

만 남게 되어 value라는 값 자체가 유효하지 않은 코드가 할당된 것처럼 보인다. 하지만 실제로 코드를 실행해보면 코드는 다음과 같은 순서로 작동한다.

  1. outer 함수가 호출된다.
  2. inner 함수가 반환되어 getInner 변수에 할당 된다.
  3. getInner 함수가 호출된다.
  4. getInner 함수는 지역 스코프 내에서 value를 탐색하고, 존재하지 않아 탐색에 실패한다.
  5. getInner 함수에 할당된 inner 함수가 선언된 시점의 렉시컬 환경을 기억하여 해당 환경에서 렉시컬 스코핑이 이루어진다.
  6. 기억된 환경에서의 렉시컬 스코핑에 따라 상위 스코프인 outer에서 value를 탐색한다.
  7. value를 발견하였으므로 "outer"를 출력한다.

이렇듯 getInner 함수가 자신이 생성될 당시에 할당된 inner 함수의 렉시컬 환경을 기억하고, 그 안에서 스코핑이 가능한 것을 클로저라고 부른다.

클로저의 활용

자바스크립트에서 클로저는 다양하게 활용될 수 있으며 이를 통해 효율적인 코드를 작성할 수 있다.

상태 유지

클로저를 활용하면 현재 상태를 기억하고 변경된 최신 상태를 유지할 수 있다. 이는 클로저가 생성될 당시의 렉시컬 환경을 기억하고, 따라서 그 기억된 환경은 소멸되지 않은 채로 최신 상태를 계속해서 유지한다는 점을 활용한 것이다.

예시

토글 버튼을 누를 때마다 문구가 사라졌다가 나타나도록 하는 코드를 작성해볼 수 있다.

<!-- HTML -->
<div class="content">토글 버튼을 눌러보세요.</div>
<button class="toggleButton">토글</button>
// JS
const content = document.querySelector(".content");
const toggleButton = document.querySelector(".toggleButton");

const onToggleClick = (function () {
  let isVisible = false;

  return function () {
    content.style.display = isVisible ? "block" : "none";

    isVisible = !isVisible;
  };
})();

toggleButton.onclick = onToggleClick;

이 코드의 작동 방식은 다음과 같다.

const onToggleClick = (function () {
  let isVisible = false;

  return function () {
    content.style.display = isVisible ? "block" : "none";

    isVisible = !isVisible;
  };
})(); // 즉시실행

onToggleClick 변수엔 내부 함수를 즉시 반환하고 소멸하는 함수가 할당되어 있다. 이 때, 반환된 함수는 자신이 생성될 때의 렉시컬 환경을 기억하므로 isVisible을 기억하는 클로저이다.

toggleButton.onclick = onToggleClick;

onToggleClick에 할당된 클로저를 toggleButtononclick 이벤트 프로퍼티에 할당했다. 따라서 onclick에 할당된 클로저는 프로퍼티에서 제거되기 전까지 자신의 렉시컬 환경을 계속해서 기억하기 때문에 isVisible 또한 소멸하지 않고 현재 상태를 기억한다.

const toggleButton = document.querySelector(".toggle");

버튼을 클릭하면 onclick에 할당된 클로저가 호출된다. isVisible의 값은 클로저에 의해 참조가 가능하기 때문에 변경이 이루어지고, 렉시컬 환경 속에서 변경된 최신 상태가 계속해서 유지된다.

전역 변수 사용 축소

전역에서 변수를 관리한다는 것은 이 변수에 누구나 접근하고 변경할 수 있다는 의미이다. 따라서 의도치 않은 동작이 이루어질 수 있고 이는 버그로 이어질 수 있는데, 클로저를 활용하면 이러한 전역 변수 사용을 줄일 수 있다.

예시

버튼을 누를 때마다 숫자가 1씩 증가하는 카운터를 만들어 볼 수 있다.

<!-- html -->
<p class="count">0</p>
<button class="increaseButton">+1</button>
// js
const count = document.querySelector(".count");
const increaseButton = document.querySelector(".increaseButton");

let current = 0;

function onIncreaseClick() {
  current += 1;
  return current;
}

increaseButton.onclick = function () {
  count.innerHTML = onIncreaseClick();
};

이 코드의 경우 문제 없이 작동한다. 하지만 current 변수가 전역에 선언되어 있기 때문에 어디에선가 접근이 이루어져 값이 변경될 위험이 높다. 그렇게 될 경우 0부터 시작되어야 할 카운터가 제대로 작동하지 않을 수 있으며, 더한 경우 동작 자체가 불가능한 버그까지 발생할 수 있다.

만약 current 변수를 increase 함수 내부로 옮기고, 클로저를 반환하면 이러한 버그를 방지할 수 있다.

// js
const count = document.querySelector(".count");
const increaseButton = document.querySelector(".increaseButton");

const onIncreaseClick = (function () {
  let current = 0;

  return function () {
    current += 1;
    return current;
  };
})();

increaseButton.onclick = function () {
  count.innerHTML = onIncreaseClick();
};

이 코드의 작동 방식은 다음과 같다.

const onIncreaseClick = (function () {
  let current = 0;

  return function () {
    current += 1;
    return current;
  };
})();

onIncreaseClick 변수엔 내부 함수를 즉시 반환하고 소멸하는 함수가 할당되어 있다. 이 때, 반환된 함수는 자신이 생성될 때의 렉시컬 환경을 기억하므로 current를 기억하는 클로저이다.

increaseButton.onclick = function () {
  count.innerHTML = onIncreaseClick();
};

increaseButtononclick 프로퍼티에 onIncreaseClick을 호출하는 이벤트핸들러를 할당하였다. 클로저인 onIncreaseClick은 호출이 될 때마다 자신이 선언됐을 때의 렉시컬 환경을 기억하여 current 변수를 참조하게 되며 current 변수는 자신을 참조하는 함수가 제거되지 않는 한 계속 존재하게 된다.

const increaseButton = document.querySelector(".increaseButton");

따라서 버튼이 클릭될 때마다 current 변수가 참조 되어 문제 없이 작동하며 외부로부터의 변경에서 안전해졌다.

참고: MDN - Closures
참고: PoiemaWeb

Categories:

Updated:

Published: