클로저 (Closure)

한눈에 보기

클로저는 함수가 선언된 시점의 렉시컬 환경을 기억해 스코프 밖에서도 변수에 접근하게 하는 JavaScript 특성이다. 동작 원리, 카운터 예시, 데이터 은닉 활용, var와 let의 루프 함정까지 정리한다.

최종 업데이트 2026.09.17

클로저(Closure)는 함수가 자신이 선언된 시점의 렉시컬 환경(Lexical Environment)을 기억하여, 그 함수가 원래의 스코프 밖에서 호출되더라도 선언 당시의 변수에 계속 접근할 수 있게 하는 JavaScript의 특성이다. 클로저는 외부에서 직접 건드릴 수 없는 상태를 함수 안에 가두는 수단이 되기 때문에, 데이터 은닉과 상태 유지, 콜백 패턴 등 JavaScript 프로그래밍 전반에서 핵심 도구로 쓰인다.

클로저가 만들어지는 원리

JavaScript는 렉시컬 스코프(Lexical Scope) 규칙을 따른다. 함수의 상위 스코프는 함수가 어디서 호출되었는지가 아니라 어디서 정의되었는지에 따라 결정된다는 뜻이다. 함수 객체는 생성되는 순간 자신이 정의된 환경에 대한 참조를 내부에 저장하고, 이후 실행될 때마다 이 참조를 따라 올라가며 외부 변수를 찾는다.

보통 함수 실행이 끝나면 그 함수의 지역 변수는 더 이상 접근할 방법이 없으므로 가비지 컬렉션(Garbage Collection)의 대상이 된다. 그러나 내부 함수가 외부 함수의 변수를 참조한 채로 반환되거나 다른 곳에 전달되면, 그 변수는 내부 함수가 살아 있는 동안 메모리에 함께 살아남는다. 이렇게 함수와 그 함수가 기억하는 환경의 결합을 클로저라고 부른다.

동작 방식을 보여주는 코드

가장 전형적인 예시는 카운터 함수다. 외부 함수 createCounter의 실행이 끝난 뒤에도 반환된 내부 함수는 count 변수에 접근하고 값을 갱신할 수 있다.

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

const counterA = createCounter();
const counterB = createCounter();

console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1, 서로 다른 환경을 갖는다

counterA와 counterB가 서로 다른 값을 세는 이유는, createCounter가 호출될 때마다 새로운 렉시컬 환경이 만들어지고 각 내부 함수가 자신만의 환경을 기억하기 때문이다. 즉 클로저는 함수 단위의 독립적인 상태 저장소 역할을 한다.

클로저의 대표적인 활용

첫째, 데이터 은닉이다. count처럼 외부에서 직접 수정할 수 없는 변수를 만들고, 공개하고 싶은 동작만 함수로 노출하면 접근 제어자가 없는 JavaScript에서도 캡슐화(Encapsulation)를 구현할 수 있다. ES2015 이전에 널리 쓰이던 모듈 패턴(Module Pattern)이 바로 이 원리다.

둘째, 함수 팩토리와 부분 적용이다. 특정 값을 미리 기억해 두었다가 나중에 사용하는 함수를 손쉽게 만들 수 있다.

const multiplyBy = (factor) => (value) => value * factor;
const double = multiplyBy(2);
console.log(double(7)); // 14

셋째, 비동기 콜백에서의 상태 유지다. 이벤트 핸들러나 setTimeout 콜백은 등록 시점과 실행 시점이 다르지만, 클로저 덕분에 등록 당시의 변수를 실행 시점에도 그대로 사용할 수 있다.

주의할 점

클로저가 참조하는 변수는 가비지 컬렉션에서 제외되므로, 큰 객체를 참조하는 클로저를 오래 유지하면 메모리 누수(Memory Leak)와 비슷한 상황이 생길 수 있다. 더 이상 필요 없는 핸들러는 해제하고, 클로저가 붙잡는 데이터의 크기를 의식하는 습관이 필요하다.

반복문에서 var를 쓸 때의 함정도 클로저와 관련된 단골 문제다. var는 함수 스코프이므로 루프의 모든 콜백이 하나의 변수를 공유하지만, let은 블록 스코프라서 반복마다 새로운 바인딩이 만들어진다.

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

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j)); // 0, 1, 2
}
구분varlet
스코프 단위함수 스코프블록 스코프
루프에서의 클로저모든 콜백이 같은 변수를 공유반복마다 새 바인딩을 캡처
위 예시의 출력3, 3, 30, 1, 2

면접에서 이렇게 물어본다

  • 클로저가 무엇인지 정의하고, 렉시컬 스코프와 어떤 관계인지 설명해 보라.
  • 외부 함수의 실행이 끝났는데도 내부 함수가 외부 변수에 접근할 수 있는 이유는 무엇인가.
  • 클로저를 이용해 데이터 은닉을 구현하는 코드를 직접 작성해 보라.
  • for 문에서 var와 let을 사용했을 때 setTimeout 출력이 달라지는 이유를 설명해 보라.
  • 클로저가 메모리 관리 측면에서 일으킬 수 있는 문제와 대처 방법은 무엇인가.
JavaScript / Python 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기