개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

클로저 — 렉시컬 환경을 기억하는 함수

2025년 8월 26일
JavaScript클로저스코프캡슐화

함수가 실행을 마치면 그 안의 변수는 사라져야 합니다. 그런데 내부에서 만든 함수를 반환받아 나중에 호출하면, 이미 끝난 외부 함수의 변수에 여전히 접근할 수 있습니다. JavaScript가 함수와 그 주변 변수들 사이의 연결을 끊지 않고 유지하기 때문입니다.

클로저란 무엇인가

클로저(Closure)는 함수와 그 함수가 선언된 시점의 렉시컬 환경(Lexical Environment)을 함께 기억하는 조합입니다. 내부 함수는 자신이 만들어진 시점의 외부 스코프 변수들을 계속 참조할 수 있으며, 외부 함수의 실행이 끝나 콜 스택에서 제거된 뒤에도 마찬가지입니다.

기본 예시 — 외부 함수 종료 후 변수 접근

function outer() {
  const message = "안녕하세요";

  function inner() {
    console.log(message); // outer가 끝난 뒤에도 접근 가능
  }

  return inner;
}

const greet = outer(); // outer 실행 완료, 콜 스택에서 제거
greet();               // "안녕하세요" — message가 살아 있음

outer는 호출이 끝난 뒤 콜 스택에서 사라집니다. 하지만 greet를 호출하면 message에 여전히 접근할 수 있습니다. inner가 message를 참조하고 있기 때문에 엔진이 해당 렉시컬 환경을 메모리에서 해제하지 않습니다.

동작 원리 — 렉시컬 환경 참조 구조

[ outer: returned ]
       |
       | return
       ▼
[ inner: function object ]
       |
       | [[Environment]] ref
       ▼
[ outer: lexical env ]
  ┌──────────────────┐
  │ message: "Hello" │
  └──────────────────┘
       |
       | outer ref
       ▼
[ global: lexical env ]

inner 함수 객체는 내부 슬롯 [[Environment]] 에 outer의 렉시컬 환경에 대한 참조를 보관합니다. 이 참조가 살아 있는 한 가비지 컬렉터는 해당 환경을 수거하지 않습니다.

가비지 컬렉터(Garbage Collector): 더 이상 참조되지 않는 메모리를 자동으로 회수하는 JavaScript 엔진의 메모리 관리 시스템입니다.

상태를 기억하는 함수 — makeCounter 패턴

클로저의 가장 실용적인 용도 중 하나는 호출할 때마다 상태를 유지하는 함수를 만드는 것입니다.

function makeCounter() {
  let count = 0;

  return {
    increment() { count += 1; },
    decrement() { count -= 1; },
    value()     { return count; },
  };
}

const counter = makeCounter();
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.value()); // 1

makeCounter를 호출할 때마다 독립적인 count 변수가 생성됩니다. 반환된 객체의 메서드들은 각자 자신이 만들어진 렉시컬 환경의 count를 참조하므로, 두 카운터 인스턴스는 서로 간섭하지 않습니다.

const counterA = makeCounter();
const counterB = makeCounter();

counterA.increment();
counterA.increment();
console.log(counterA.value()); // 2
console.log(counterB.value()); // 0 — 독립적

데이터 캡슐화 — 외부에서 직접 접근 불가한 변수

클로저를 이용하면 내부 구현을 숨기고 정해진 인터페이스를 통해서만 데이터를 다루도록 제한하는 캡슐화를 구현할 수 있습니다. 외부에서 직접 읽거나 쓸 수 없는 변수가 그 예입니다.

캡슐화(Encapsulation): 객체지향 설계의 핵심 원칙 중 하나로, 내부 상태를 숨기고 외부에는 정해진 인터페이스만 노출합니다.

function makeUser(name) {
  let _loginCount = 0; // 외부에서 직접 접근 불가

  return {
    getName()  { return name; },
    login()    { _loginCount += 1; },
    getCount() { return _loginCount; },
  };
}

const user = makeUser("홍길동");
user.login();
user.login();
console.log(user.getCount());   // 2
console.log(user._loginCount);  // undefined — 직접 접근 차단

_loginCount는 makeUser의 렉시컬 환경 안에만 존재합니다. 반환된 객체에 포함되지 않았으므로 외부 코드는 이 값을 직접 조작할 수 없습니다.

반복문 클로저 함정 — var vs let

반복문 안에서 클로저를 만들 때 var와 let은 전혀 다르게 동작합니다.

var — 의도한 대로 동작하지 않음

const funcs = [];

for (var i = 0; i < 3; i++) {
  funcs.push(function () {
    console.log(i);
  });
}

funcs[0](); // 3
funcs[1](); // 3
funcs[2](); // 3

var로 선언한 i는 함수 스코프입니다. 반복문이 끝난 뒤 i는 3이 되고, 세 함수 모두 같은 렉시컬 환경의 i를 참조하므로 전부 3을 출력합니다.

let — 반복마다 독립적인 스코프 생성

const funcs = [];

for (let i = 0; i < 3; i++) {
  funcs.push(function () {
    console.log(i);
  });
}

funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2

let은 블록 스코프입니다. 반복마다 새로운 렉시컬 환경이 생성되어 각 함수가 서로 다른 i를 캡처합니다. 이것이 var와 let이 클로저 맥락에서 다르게 작동하는 핵심 이유입니다.

선언스코프반복마다 새 환경출력 결과
var함수아니오전부 3
let블록예0, 1, 2

메모리 주의사항

클로저는 렉시컬 환경을 참조로 붙들기 때문에, 큰 데이터를 담은 외부 변수가 함수와 함께 살아남아 메모리 누수로 이어질 수 있습니다.

메모리 누수(Memory Leak): 더 이상 필요하지 않은 데이터가 참조 때문에 가비지 컬렉터에 의해 회수되지 못하고 메모리를 계속 점유하는 현상입니다.

function processLargeData() {
  const bigArray = new Array(1_000_000).fill("데이터");

  return function getValue(index) {
    return bigArray[index]; // bigArray 전체가 메모리에 유지됨
  };
}

const getValue = processLargeData();
// getValue가 살아 있는 한 bigArray(약 8MB)는 수거되지 않음

필요한 값만 추출해 참조를 끊는 것이 좋습니다.

function processLargeData() {
  const bigArray = new Array(1_000_000).fill("데이터");
  const result = bigArray[0]; // 필요한 값만 복사

  return function getValue() {
    return result; // bigArray는 참조 없음 → 수거 가능
  };
}

요약

개념핵심
클로저함수 + 선언 시점의 렉시컬 환경 참조
[[Environment]]함수 객체가 렉시컬 환경을 보관하는 내부 슬롯
상태 유지외부 함수 종료 후에도 내부 함수가 변수를 참조
캡슐화반환 객체에 포함되지 않은 변수는 외부에서 접근 불가
반복문 함정var는 공유 스코프, let은 반복마다 독립 스코프 생성

클로저가 동작하는 이유는 함수 객체가 자신이 만들어진 렉시컬 환경에 대한 참조를 [[Environment]] 슬롯에 보관하기 때문입니다. 외부 함수가 종료된 뒤에도 내부 함수가 변수를 기억하는 것은 이 참조가 끊어지지 않기 때문이며, 상태 유지와 캡슐화가 가능한 것도 같은 원리입니다.

← 이전 글this — 호출 방식과 바인딩 규칙
다음 글 →프로토타입 — JavaScript 상속의 실제 구조