frontend/javascript

🟨 2-18. 클로저(Closure) 완전 정복 — 함수형 자바스크립트의 핵심 개념과 실무 응용

mirabo01 2025. 11. 7. 08:53

1. 클로저란 무엇인가

MDN에서는 이렇게 정의한다 👇

“클로저는 함수와 그 함수가 선언될 때의 렉시컬 환경(Lexical Environment) 의 조합이다.”

즉, 함수가 자신이 선언된 스코프(환경)를 기억하는 현상이 바로 클로저다.

쉽게 말해,
“함수가 태어난 그 순간의 기억을 평생 가지고 있는 것.”


2. 기본 예시

function outer() {
  const name = "기범";
  function inner() {
    console.log(name);
  }
  return inner;
}

const fn = outer();
fn(); // 기범

✅ outer()가 끝나서 보통이라면 name 변수는 사라져야 한다.
✅ 하지만 inner()가 name을 기억하고 있음 → 이것이 클로저!


3. 왜 클로저가 만들어질까?

자바스크립트는 렉시컬 스코프(Lexical Scope) 를 따르기 때문이다.

즉, 함수가 “호출된 위치”가 아니라 “선언된 위치”를 기준으로
변수의 유효 범위를 결정한다.

const x = 1;
function outer() {
  const x = 2;
  function inner() {
    console.log(x);
  }
  return inner;
}
const fn = outer();
fn(); // 2

✅ inner()는 자신이 “outer 안에서” 정의됐기 때문에
outer의 변수 x=2를 기억하고 있다.


4. 클로저의 실전 활용 — 데이터 은닉

클로저는 정보를 숨기고 제어하는 데 자주 사용된다.

function createCounter() {
  let count = 0;
  return {
    increase() {
      count++;
      console.log(count);
    },
    decrease() {
      count--;
      console.log(count);
    },
  };
}

const counter = createCounter();
counter.increase(); // 1
counter.increase(); // 2
counter.decrease(); // 1

✅ count 변수는 외부에서 직접 접근 불가
✅ 오직 increase, decrease 함수를 통해서만 조작 가능

클래스 없이도 캡슐화(Encapsulation) 구현 가능!


5. 클로저와 메모리 관리

클로저는 변수를 “기억”하기 때문에
불필요한 메모리 점유가 발생할 수도 있다.

function heavy() {
  const bigData = new Array(1000000).fill("data");
  return function () {
    console.log(bigData.length);
  };
}

const fn = heavy(); // bigData는 여전히 메모리에 남아있음

✅ 클로저가 참조 중인 변수는 GC(가비지 컬렉션) 대상에서 제외됨
✅ 따라서 불필요한 클로저는 제거하거나 null 처리해야 함

fn = null; // 참조 해제 → bigData 가비지 컬렉션 가능

6. 클로저와 루프

아래 코드를 보면 자주 발생하는 함정이 있다 👇

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000);
}

출력 결과:

3
3
3

이유는?
var는 함수 스코프라서, 모든 함수가 동일한 i를 공유하기 때문.

해결 방법 2가지 👇

✅ 1) let 사용 (블록 스코프)

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000);
}
// 0 1 2

✅ 2) 즉시 실행 함수 (IIFE)

for (var i = 0; i < 3; i++) {
  ((n) => setTimeout(() => console.log(n), 1000))(i);
}

클로저를 이용해 각 루프의 i 값을 고유하게 캡처한 것이다.


7. 클로저로 상태 유지하기

클로저는 비동기 함수에서도 상태를 유지하는 데 유용하다.

function delayedPrinter() {
  let count = 0;
  return function () {
    setTimeout(() => {
      count++;
      console.log(`출력 횟수: ${count}`);
    }, 500);
  };
}

const print = delayedPrinter();
print(); // 출력 횟수: 1
print(); // 출력 횟수: 2

✅ count는 외부에서 접근 불가하지만,
✅ 함수 호출 시마다 내부적으로 상태가 누적된다.


8. 클로저와 함수형 프로그래밍

클로저는 함수형 프로그래밍의 핵심이기도 하다.
불변성과 순수 함수 개념을 유지하면서,
“상태를 함수 안에 숨긴다.”

function makeAdder(x) {
  return function (y) {
    return x + y;
  };
}

const add5 = makeAdder(5);
console.log(add5(10)); // 15

✅ x=5라는 환경이 클로저에 저장됨
✅ add5는 5를 더하는 함수를 영구히 기억한다

➡ 이런 패턴은 React의 hook, Redux의 reducer 등에서도 자주 등장한다.


9. 실무에서 자주 쓰이는 클로저 패턴

목적 예시

상태 유지 카운터, 타이머, 애니메이션 상태
정보 은닉 private 변수, 데이터 보호
함수 팩토리 makeAdder, createValidator
이벤트 핸들러 특정 컨텍스트를 유지하는 콜백
React Hooks 내부 구현 useState, useEffect 등 상태 저장 로직

예를 들어, React의 useState도 내부적으로 클로저를 활용한다.

function useState(initialValue) {
  let state = initialValue;
  function setState(newValue) {
    state = newValue;
    render();
  }
  return [state, setState];
}

✅ 외부에서 state에 직접 접근 불가
✅ setState로만 변경 가능 — 완벽한 캡슐화 구조


10. 디버깅 시 주의할 점

개발자 도구에서 클로저를 디버깅하면
변수가 사라지지 않고 남아 있는 것을 볼 수 있다.

✅ 크롬 DevTools → Sources → Scope → Closure 영역 확인

불필요한 참조가 많을수록 메모리 누수 가능성이 커진다.
→ 특히 이벤트 리스너나 타이머 내부 클로저는 주의!

element.addEventListener("click", () => {
  console.log(heavyData);
});
// 해제하지 않으면 heavyData 계속 유지됨
element.removeEventListener("click", handler);

11. 정리

개념 설명

클로저 함수가 자신이 선언된 환경(변수)을 기억하는 현상
핵심 원리 렉시컬 스코프
주요 특징 상태 유지, 정보 은닉, 함수형 패턴
장점 데이터 보호, 모듈화, 재사용성
주의점 메모리 누수 가능성, 과도한 사용 지양

12. 마무리

클로저는 함수형 자바스크립트의 핵심이며,
React, Vue, Node 등 모든 환경에서 사용되는 개념이야.
이걸 제대로 이해하면, “왜 이렇게 동작하지?”라는 의문이 대부분 해결된다.

“클로저를 이해하면 자바스크립트가 단순한 스크립트 언어에서
구조적인 언어로 보이기 시작한다.”