frontend/javascript

🟨 1-10. 클로저(Closure)와 실행 컨텍스트 심화 — 함수가 변수를 기억하는 원리

mirabo01 2025. 11. 6. 22:08

1. 들어가며

자바스크립트에서 함수는 단순히 “코드 묶음”이 아니라,
자신이 선언된 환경(스코프)을 기억하는 살아 있는 객체다.

이 특성을 가능하게 하는 것이 바로 클로저(Closure) 다.

한마디로 정리하면 이렇다.

“클로저란 함수가 선언될 때의 환경을 기억하는 기능이다.”

이제 이 문장이 무슨 뜻인지 예제를 통해 하나씩 풀어보자.


2. 함수는 함수를 반환할 수 있다

자바스크립트의 함수는 일급 객체이므로, 다른 함수 안에서 생성하고 반환할 수 있다.

function outer() {
  const name = "기범";

  function inner() {
    console.log(`안녕하세요, ${name}`);
  }

  return inner;
}

const greet = outer();
greet(); // "안녕하세요, 기범"

여기서 놀라운 점은,
outer()가 이미 실행을 마쳤음에도 inner()는 여전히 name에 접근할 수 있다는 것이다.

보통 함수가 끝나면 그 안의 변수는 메모리에서 사라져야 하지만,
inner 함수가 해당 변수를 참조하고 있기 때문에 자바스크립트 엔진은 그 값을 메모리에 유지한다.

이 현상이 바로 클로저다.


3. 클로저의 핵심 동작 원리

이걸 이해하려면 실행 컨텍스트(Execution Context) 를 알아야 한다.

자바스크립트 엔진은 함수를 실행할 때

  1. 실행 컨텍스트를 생성하고
  2. 그 안에 변수 환경(Variable Environment)을 만든다.

함수가 종료되면 컨텍스트는 사라지지만,
내부 함수가 외부 변수에 접근 중이라면 그 변수는 메모리에서 유지된다.

즉, “필요한 데이터만 살아남는 스냅샷”이 만들어지는 것이다.


4. 클로저의 실제 예시 — 상태 유지

function counter() {
  let count = 0;

  return function () {
    count++;
    console.log(`현재 카운트: ${count}`);
  };
}

const increase = counter();
increase(); // 1
increase(); // 2
increase(); // 3

위 예제에서 count 변수는 counter()가 종료된 뒤에도 사라지지 않는다.
increase 함수가 여전히 그 값을 참조하고 있기 때문이다.

이 덕분에 클로저를 이용하면 함수 내부에 상태를 저장할 수 있다.


5. 클로저의 응용 — private 변수 만들기

클로저는 자바스크립트에서 정보 은닉(encapsulation)을 구현할 때 매우 유용하다.

function makeCounter() {
  let count = 0;

  return {
    increase: function() {
      count++;
      console.log(count);
    },
    decrease: function() {
      count--;
      console.log(count);
    },
    getCount: function() {
      return count;
    },
  };
}

const counter1 = makeCounter();
counter1.increase(); // 1
counter1.decrease(); // 0
console.log(counter1.getCount()); // 0

count는 외부에서 직접 접근할 수 없고,
increase나 decrease 같은 내부 메서드를 통해서만 조작할 수 있다.

이런 방식은 클래스 없이도 데이터를 보호하는 방법으로,
리액트 훅(useState)이나 커스텀 훅의 내부 동작 원리에도 사용된다.


6. 클로저의 메모리 유지 원리

클로저는 필요할 때만 변수를 메모리에 유지한다.
하지만 클로저가 너무 많거나 오래 남아 있으면 메모리 누수가 생길 수도 있다.

function heavyTask() {
  const bigArray = new Array(1000000).fill("data");

  return function() {
    console.log(bigArray[0]);
  };
}

const task = heavyTask(); // bigArray가 계속 메모리에 남음

이 경우 task가 계속 살아있는 한 bigArray도 메모리에 남는다.
즉, 클로저는 “필요할 때만 사용하고, 참조가 끝나면 제거”해야 한다.


7. 자주 발생하는 실수 — 루프 안의 클로저

클로저를 잘못 이해하면 이런 버그가 자주 생긴다.

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

결과는 0, 1, 2가 아니라 3, 3, 3이 출력된다.
이유는 var가 함수 스코프이기 때문에,
모든 함수가 하나의 동일한 i를 참조하기 때문이다.

해결 방법은 let을 사용하거나 즉시 실행 함수를 활용하는 것이다.

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

let은 블록 스코프를 가지므로
각 반복마다 i의 독립적인 클로저가 생성된다.


8. 클로저와 React useState의 관계

리액트의 useState 훅은 내부적으로 클로저를 이용해 상태를 유지한다.

function useState(initialValue) {
  let state = initialValue;

  function setState(newValue) {
    state = newValue;
    render(); // 가상 렌더링
  }

  return [() => state, setState];
}

이 함수가 호출될 때마다 state 값은 클로저 안에 저장되어
컴포넌트가 재렌더링되어도 값이 유지된다.

즉, 클로저 덕분에 상태가 외부로부터 보호되며,
리액트가 선언형으로 동작할 수 있는 기반이 된다.


9. 실행 컨텍스트의 전체 흐름

  1. 함수 선언
    • 스코프(lexical environment) 결정
    • 변수와 함수 선언 등록
  2. 함수 실행
    • 실행 컨텍스트 생성
    • 변수 환경(Variable Environment) 활성화
  3. 클로저 생성
    • 내부 함수가 외부 변수 참조 → 해당 환경 유지
  4. 함수 종료 후에도 외부 변수는 살아 있음

결국 클로저는 “실행 컨텍스트의 생명 연장자”라고 할 수 있다.


10. 정리

개념 설명 실무 활용

클로저 함수가 선언 당시의 스코프를 기억하는 기능 상태 관리, 정보 은닉
실행 컨텍스트 함수 실행 시 생성되는 환경 객체 스코프와 변수 관리
메모리 유지 참조 중인 변수만 남음 효율적이나 누수 주의
블록 스코프(let) 루프 내 클로저 버그 방지 for문, 이벤트 리스너
실무 예시 useState, 이벤트 핸들러, 비동기 함수 리액트, 비동기 처리

11. 마무리

클로저는 자바스크립트를 깊이 이해하는 순간 열리는 문이다.
그냥 “함수가 변수를 기억한다”는 수준에서 멈추지 말고,
왜 기억하는지, 언제 기억을 끊어야 하는지까지 알아야 진짜 실무에서 자유롭게 쓸 수 있다.

클로저는 함수의 메모리이며, 상태의 집이다.
자바스크립트의 유연함은 결국 클로저로부터 나온다.


다음 편에서는
1-11. 비동기(Asynchronous)와 이벤트 루프(Event Loop) — 자바스크립트가 멈추지 않는 이유
를 통해 실제 코드가 어떻게 동시에 여러 작업을 처리하는지,
그리고 Promise, async/await가 어떻게 그 흐름을 제어하는지 구체적으로 살펴보자.