Oneul Code - 14. JavaScript Closures: What Are They?

2025-10-21

1. 클로저란?

클로저(Closure): 함수가 자신이 선언될 당시의 스코프(lexical scope)를 기억하고,
그 스코프 밖에서 해당 함수가 실행되더라도 그 변수들에 접근할 수 있는 현상을 말합니다.

쉽게 풀어서 설명하자면
“내부 함수가 외부 함수의 변수에 접근할 수 있는 것” 입니다.

예시:

function outer() {
  const message = "Hello, Closure!";

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

  return inner;
}

const sayHello = outer();
sayHello(); // ✅ "Hello, Closure!"

여기서 inner()는 outer()가 이미 실행을 마쳐서 스코프가 끝난 이후에도, message 변수에 접근할 수 있습니다. 이게 바로 클로저의 핵심입니다.

즉, “함수가 만들어질 때의 환경을 기억하는 함수”가 클로저입니다.


2. 클로저의 특징

2-1. 함수가 선언될 때의 스코프를 기억한다.

function greeting(msg) {
  return function who(name) {
    console.log(`${name}님, ${msg}!`);
  };
}

var hello = greeting("안녕하세요");
var howdy = greeting("잘 지내시나요");

hello("카일"); // 카일 님, 안녕하세요!
hello("보라"); // 보라 님, 안녕하세요!
howdy("호진"); // 호진 님, 잘 지내시나요!

이 예제의 포인트는 greeting()이 호출될 때마다 새로운 스코프와 msg 값이 생기며, 내부 함수 who()는 그 msg를 기억합니다.

hello는 “안녕하세요”라는 환경을, howdy는 “잘 지내시나요”라는 환경을 기억하고 있죠.

이처럼 클로저는 외부 함수의 실행 컨텍스트가 사라져도 변수에 대한 참조를 유지합니다.

2-2. 상태를 유지할 수 있다.(데이터 은닉)

function counter(step = 1) {
  var count = 0;

  return function increaseCount() {
    count = count + step;
    return count;
  };
}

var incBy1 = counter(1);
var incBy3 = counter(3);

incBy1(); // 1
incBy1(); // 2

incBy3(); // 3
incBy3(); // 6
incBy3(); // 9

count 변수는 counter 함수 내부에서 선언되었기 때문에 외부에서는 직접 접근할 수 없습니다. incBy1과 incBy3은 각각 counter가 반환한 독립적인 클로저입니다. 각 클로저는 자신만의 독립적인 count 변수를 기억하고 값을 누적하여 변경합니다. 서로의 count 변수에는 영향을 주지 않습니다.

객체의 프로퍼티처럼 사용되면서도 외부로부터의 직접적인 수정을 막아 데이터를 안전하게 보호(캡슐화)하는 효과를 줍니다.
실무에서 특정 상태를 관리하는 모듈 패턴 등에 유용합니다.

2-3. 비동기 콜백에서도 스코프를 기억한다

function getSomeData(url) {
  ajax(url, function onResponse(resp) {
    console.log(`${url}에서 온 응답: ${resp}`);
  });
}

getSomeData("https://some.url/wherever");

이 코드의 포인트는 onResponse가 비동기 콜백임에도 url 변수를 기억한다는 점입니다.

비동기 처리는 나중에 실행되지만, 클로저 덕분에 함수가 “당시의 url 값”을 기억합니다. 그래서 “어떤 요청에 대한 응답인지”를 잊지 않습니다.

즉, 클로저는 비동기 로직에서 변수 상태를 안전하게 유지할 수 있게 해줍니다.

2-4. 루프내에서의 클로저

for (let [idx, btn] of buttons.entries()) {
  btn.addEventListener("click", function onClick() {
    console.log(`${idx}번째 버튼을 클릭했습니다!`);
  });
}

여기서 핵심은 let입니다.. let은 블록 스코프를 가지므로, 각 반복마다 새로운 idx가 클로저에 저장됩니다.

만약 var를 사용했다면? 모든 핸들러가 마지막 idx 값만 출력합니다.

그래서 루프안에서 클로저를 사용할때에는 let 사용이 필수입니다. (ES6 이후의 큰 개선점 중 하나입니다.)


3. 실무에서의 클로저 활용

3-1. 모듈 패턴 (Module Pattern)

const CounterModule = (function () {
  let count = 0;

  return {
    increment() {
      count++;
      console.log(count);
    },
    reset() {
      count = 0;
      console.log("리셋 완료");
    },
  };
})();

CounterModule.increment(); // 1
CounterModule.increment(); // 2
CounterModule.reset(); // 리셋 완료

이 패턴은 내부 상태(count)를 외부에서 직접 접근할 수 없게 보호하면서, 특정 메서드만 노출시킵니다.

정보 은닉 + 상태 관리 + 전역 변수 오염 방지 클로저의 실무적 가치가 가장 잘 드러나는 패턴입니다.

3-2. 이벤트 핸들러 내 상태 유지

function createButtonHandler(buttonId) {
  let clickCount = 0;
  return function () {
    clickCount++;
    console.log(`${buttonId} 버튼 클릭: ${clickCount}회`);
  };
}

const btn = document.getElementById("save");
btn.addEventListener("click", createButtonHandler("save"));

createButtonHandler함수가 실행될 때, 함수내부에서 buttonId와 clickCount라는 두 변수가 포함된 고유한 렉시컬 환경이 만들어집니다. createButtonHandler는 정의된 익명함수(클릭 이벤트 핸들러)를 반환하며 실행을 마칩니다. 반환된 익명함수는 자신이 선언될 당시의 환경 즉 buttonId와 clickCount 변수를 기억합니다.

정리하자면, 위 코드는 각 버튼별로 클릭 횟수를 개별적으로 기억할 수 있습니다.


4. 클로저와 메모리 관리

메모리 누수(Memory Leak)에 대한 이해

클로저는 외부 함수의 변수를 메모리에 보존하기 때문에 강력합니다.
하지만 이로 인해 필요 이상으로 메모리를 점유하게 되면 메모리 누수를 발생시킬 수 있습니다.

예시:

  • 문제: 클로저가 더 이상 사용되지 않는 외부 스코프의 매우 큰 데이터를 계속 참조하고 있을 때.
var bigDataHolder = (function () {
  var bigData = new Array(1000000).fill(0);

  return function processData() {};
})();
  • 해결: 클로저를 사용하는 변수가 더 이상 필요하지 않을 경우, 해당 변수에 null을 할당하여 메모리 해제를 돕는 것이 좋습니다.
    (최신 JS 엔진은 대부분 최적화되어 있지만, 고전적인 패턴이나 큰 데이터를 다룰 때는 고려해야 합니다.)
// 예시: 불필요한 참조 제거
var bigDataHolder = (function () {
  var bigData = new Array(1000000).fill(0); // 엄청나게 큰 배열

  return function processData() {
    // bigData를 사용하는 로직

    if (bigData) {
      bigData = null;
    }
  };
})();

// 만약 bigDataHolder가 더 이상 필요 없다면
bigDataHolder = null; // 메모리에서 bigData 참조 해제를 도움

5. Oneul Code를 정리하며…

  • 기억해야 할 키포인트
구분 핵심 내용
정의 내부 함수가 외부 스코프(환경)를 기억하고 접근할 수 있는 현상
목적 데이터 은닉, 함수 실행 후에도 상태(데이터) 유지
활용 모듈 패턴, 비동기 콜백 함수, 이벤트 핸들러
주의 과도한 참조로 인한 메모리 누수 발생 가능성

Oneul Code는 오늘 배운 내용을 정리하면,
클로저는 “외부 변수를 참조하는 내부 함수”로 설명할 수 있지만, 진짜 핵심은 “함수의 선언 시점 스코프를 기억한다”는 것입니다.