자바스크립트 교재 · 21편 / 26편

클로저 깊이 — 함수가 함수를 기억하는 법

12편의 클로저를 본격적으로. 카운터·IIFE·모듈·메모리 누수까지.

고급읽는 시간 7분2026-05-17
외부 함수가 끝났는데도 내부 함수가 변수를 들고 있는 클로저 도식

12편에서 살짝 본 클로저를 깊이. 함수가 자기 만들어진 스코프의 변수를 들고 다니는 것 — JS 의 거의 모든 고급 기법이 클로저 위에 서 있습니다. 고급 파트의 시작.

한 줄 정의

클로저 = 함수 + 그 함수가 만들어진 스코프의 변수들. 외부 함수가 끝나도, 내부 함수가 그 변수들을 계속 보유하고 사용할 수 있음.

function makeCounter() {
  let count = 0;
  return function () {
    count++;
    return count;
  };
}

const c = makeCounter();
c();   // 1
c();   // 2
c();   // 3
// makeCounter 가 끝난 지 한참 뒤에도 count 는 살아 있음
// 외부에서는 count 못 봄 — 진짜 private

3가지 일상 패턴

① 카운터 / private 상태

function makeBank(initial = 0) {
  let balance = initial;
  return {
    deposit(n) { balance += n; },
    withdraw(n) {
      if (n > balance) throw new Error("부족");
      balance -= n;
    },
    getBalance() { return balance; },
  };
}

const acc = makeBank(1000);
acc.deposit(500);
acc.getBalance();   // 1500
acc.balance;        // undefined ← 직접 접근 불가

② 한 번만 실행 (once)

function once(fn) {
  let called = false;
  let result;
  return function (...args) {
    if (!called) {
      called = true;
      result = fn.apply(this, args);
    }
    return result;
  };
}

const initOnce = once(() => expensiveSetup());
initOnce();   // 실행
initOnce();   // 캐시된 결과
initOnce();   // 캐시된 결과

③ partial application / curry

function add(a) {
  return function (b) {
    return a + b;
  };
}

const add5 = add(5);   // a=5 를 기억한 함수
add5(3);   // 8
add5(10);  // 15

// 화살표로
const mul = (a) => (b) => a * b;
const double = mul(2);
double(7);   // 14

IIFE — 즉시 실행 함수 표현식 (옛 패턴)

// 옛 var 시절 — 새 스코프를 강제로 만들 때
(function () {
  const private_ = "hidden";
  // ... 스코프 격리
})();

// 또는
(function () { ... }());

// 현대 JS — let/const + 블록이면 됨
{
  const private_ = "hidden";
}

// 모듈(ESM) — 파일 자체가 스코프

IIFE 의 운명. ES6 의 let/const + ESM 모듈 시스템이 등장하면서 새 코드에서는 거의 안 씁니다. 라이브러리의 UMD 빌드 등 호환성 목적이 거의 유일한 사용처.

모듈 패턴 — IIFE + 클로저

// ES 모듈 이전의 캡슐화 표준
const Counter = (function () {
  let count = 0;          // 외부에서 안 보임
  return {
    inc() { count++; },
    get() { return count; },
  };
})();

Counter.inc();
Counter.get();   // 1
Counter.count;   // undefined

지금은 ESM 으로 같은 효과 — 파일 내부 변수가 자동으로 캡슐화.

반복문 + 클로저 함정 (3편 다시)

// ❌ var 시절 함정
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 0, 1, 2 가 나오길 기대했지만 → 3, 3, 3
// 이유: var 가 함수 스코프, 모든 콜백이 같은 i 를 봄
// setTimeout 시점에는 i 가 이미 3

// ✅ let — 매 반복마다 새 바인딩
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 0, 1, 2

// ✅ 옛 해결책 — IIFE 로 새 스코프
for (var i = 0; i < 3; i++) {
  (function (j) {
    setTimeout(() => console.log(j), 0);
  })(i);
}

React 등 프레임워크에서의 클로저

// React useState — 클로저로 구현
function useState(initial) {
  let value = initial;
  const setValue = (newValue) => { value = newValue; };
  return [() => value, setValue];
}
// (실제 useState 는 더 복잡 — 매 렌더 새 값 캡처)

// 이벤트 핸들러에 props 캡처 — 클로저 함정
function Component({ count }) {
  useEffect(() => {
    setInterval(() => {
      console.log(count);   // 첫 렌더의 count 만 기억 (stale)
    }, 1000);
  }, []);   // deps 빈 배열 → 첫 렌더 클로저 유지
}

// 해결 — deps 에 추가 또는 ref 패턴

메모리 누수 — 클로저의 비용

// ❌ 외부 DOM 참조를 잡고 있는 콜백 — 페이지 떠나도 메모리 유지
function bind() {
  const el = document.getElementById("big-data");
  el.addEventListener("click", () => {
    console.log(el.dataset.id);   // el 을 클로저로 잡음
  });
  // el 이 DOM 에서 제거돼도 클로저가 들고 있어서 GC 안 됨
}

// ✅ 필요한 값만 캡처
function bind() {
  const el = document.getElementById("big-data");
  const id = el.dataset.id;   // 작은 값만 캡처
  el.addEventListener("click", () => {
    console.log(id);
  });
}

// ✅ 또는 명시적으로 제거
const handler = () => { ... };
el.addEventListener("click", handler);
// 나중에
el.removeEventListener("click", handler);

메모리 누수 진단. 브라우저 DevTools → Memory → Heap snapshot. 시간 차로 두 번 찍어 비교하면 안 사라지는 객체가 보입니다. SPA(React·Vue) 의 가장 흔한 누수가 정리 안 된 이벤트 리스너.

클로저 vs 클래스 — 캡슐화 비교

// 클로저
function makeUser(name) {
  let _name = name;
  return {
    getName() { return _name; },
    setName(n) { _name = n; },
  };
}

// 클래스 + #private
class User {
  #name;
  constructor(name) { this.#name = name; }
  getName() { return this.#name; }
  setName(n) { this.#name = n; }
}

실전 선택. ① 인스턴스 1~소수 → 클로저(가볍고 직관). ② 다수 인스턴스·상속 → 클래스(메서드가 프로토타입에 한 번만 저장돼 메모리 효율). ③ React 같은 함수형 패러다임 → 클로저 + 훅.

한 줄 정리

  • 클로저 = 함수 + 외부 스코프의 변수 기억.
  • private 상태·once·partial·메모이즈의 기반.
  • let/const + 모듈로 옛 IIFE 의 90% 는 없어졌음.
  • 큰 객체를 캡처하면 메모리 누수 — 필요한 값만.
  • React 의 stale closure 함정 — deps 또는 ref.

22편 — this 의 정체 (4가지 호출 방식)

전역/메서드/생성자/명시 호출. 화살표 함수의 this 가 다른 이유.

📚 쉽게 배우는 자바스크립트 교재
이전: 20편 정규식 · 현재: 21편 (고급 시작) · 다음 → 22편 this · 진행: 21/26

© 2026 주나이테크(주) @JUNAITECH