2025-09-22 00:48

  • 클로저는 함수가 선언될 때의 렉시컬 환경을 기억하여, 함수가 외부에서 호출될 때도 해당 환경에 접근할 수 있게 하는 핵심 개념이다.

  • 클로저는 내부 함수가 외부 함수의 변수에 접근할 수 있게 하여 상태를 은닉하고, 특정 함수에 종속된 비공개 변수를 만드는 등 고급 프로그래밍 패턴을 가능하게 한다.

  • 메모리 누수의 원인이 될 수 있으므로, 클로저로 인해 더 이상 필요 없는 외부 변수에 대한 참조가 남아있지 않도록 신중한 관리가 필요하다.


자바스크립트 클로저 완벽 핸드북 개발자 성장의 비밀 열쇠

자바스크립트는 현대 웹 개발의 심장과도 같은 언어다. 그리고 그 심장을 뛰게 하는 가장 중요한 판막 중 하나가 바로 **클로저(Closure)**다. 많은 개발자가 클로저의 개념을 어렴풋이 알고 있지만, 그 본질과 활용법을 정확히 꿰뚫고 있는 경우는 드물다. 클로저는 단순히 ‘내부 함수가 외부 함수의 변수에 접근하는 것’이라는 한 문장으로 정의하기엔 너무나 깊고 강력한 메커니즘이다. 이 핸드북은 클로저의 탄생 배경부터 구조, 실제 사용법, 그리고 심화 내용까지, 당신이 클로저를 완벽하게 정복하고 한 단계 높은 수준의 자바스크립트 개발자로 거듭날 수 있도록 안내할 것이다.

1. 클로저, 왜 만들어졌는가? 프로그래밍 언어의 오랜 숙원

클로저의 개념을 이해하려면 먼저 **렉시컬 스코프(Lexical Scope)**를 알아야 한다. 자바스크립트를 포함한 대부분의 프로그래밍 언어는 렉시컬 스코프, 즉 ‘정적 스코프’ 규칙을 따른다. 이는 변수의 유효 범위가 함수가 호출되는 시점이 아니라, 함수가 선언되는 시점에 결정된다는 의미다.

JavaScript

const globalVar = '전역';

function outer() {
  const outerVar = '외부';

  function inner() {
    const innerVar = '내부';
    console.log(globalVar); // '전역'
    console.log(outerVar);  // '외부'
    console.log(innerVar);  // '내부'
  }

  inner();
}

outer();

위 코드에서 inner 함수는 자신이 선언된 위치를 기준으로 접근할 수 있는 모든 변수(innerVar, outerVar, globalVar)에 접근할 수 있다. 이것이 바로 렉시컬 스코프의 핵심이다.

하지만 프로그래밍을 하다 보면, 함수가 선언된 렉시컬 스코프를 벗어나 다른 곳에서 호출될 때에도 원래의 스코프에 접근해야 하는 필요성이 생긴다. 예를 들어, 어떤 함수의 실행이 끝난 후에도 그 함수 내부의 특정 상태(변수)를 계속 기억하고 사용하고 싶을 때가 있다. 바로 이 지점에서 클로저의 필요성이 대두된다.

클로저는 “함수와 그 함수가 선언될 당시의 렉시컬 환경(Lexical Environment)의 조합”이다. 말이 조금 어렵지만, 쉽게 비유하자면 **‘자신이 태어난 환경을 기억하는 함수’**라고 할 수 있다. 함수가 다른 곳으로 전달되고 실행될 때, 마치 고향의 추억과 정보를 가방에 담아 가지고 다니는 것처럼, 자신이 선언되었던 환경의 변수들을 계속 참조할 수 있는 것이다.

이러한 특성 덕분에 프로그래머들은 상태를 안전하게 숨기고(정보 은닉), 특정 함수에 종속된 비공개(private) 변수를 만드는 등의 정교한 프로그래밍 패턴을 구현할 수 있게 되었다.

2. 클로저의 구조 해부 자바스크립트 엔진의 마법

클로저는 어떻게 자신이 태어난 환경을 기억할 수 있을까? 그 비밀은 자바스크립트 엔진이 코드를 실행하는 방식에 있다. 엔진은 함수가 호출될 때마다 해당 함수의 실행에 필요한 정보를 담는 **실행 컨텍스트(Execution Context)**라는 것을 생성한다. 이 실행 컨텍스트 안에는 렉시컬 환경(Lexical Environment) 컴포넌트가 있고, 바로 여기에 클로저의 비밀이 숨어있다.

렉시컬 환경은 두 가지 주요 부분으로 구성된다.

  1. 환경 레코드(Environment Record): 현재 스코프에 포함된 변수, 함수 선언 등의 식별자와 그 값을 기록하는 공간이다.

  2. 외부 렉시컬 환경에 대한 참조(Outer Lexical Environment Reference): 상위 스코프(자신을 포함하는 외부 함수 또는 전역 스코프)의 렉시컬 환경을 가리키는 포인터다.

이 ‘외부 렉시컬 환경에 대한 참조’가 바로 스코프 체인(Scope Chain)을 형성하는 연결고리 역할을 한다. 함수가 특정 변수를 찾을 때, 먼저 자신의 환경 레코드에서 찾아보고, 없으면 외부 렉시컬 환경 참조를 따라 상위 스코프로 이동하여 찾는 과정을 반복한다.

클로저가 발생하는 상황을 코드로 살펴보자.

JavaScript

function outer() {
  const outerVar = '외부 변수';

  function inner() { // inner 함수는 outer 함수의 실행 중에 생성된다.
    console.log(outerVar); // '외부 변수'
  }

  return inner; // inner 함수를 반환
}

const innerFunc = outer(); // outer 함수 실행 완료.
                           // 이 시점에 outer 함수의 실행 컨텍스트는 스택에서 사라진다.

innerFunc(); // 하지만 innerFunc는 여전히 outerVar에 접근할 수 있다.

여기서 마법 같은 일이 일어난다. outer 함수는 실행이 끝나서 실행 컨텍스트가 스택에서 제거되었다. 일반적으로라면 outer 함수 내부의 변수인 outerVar도 함께 사라져야 한다. 하지만 inner 함수가 outerVar를 참조하고 있기 때문에, 자바스크립트 엔진은 outerVar가 포함된 outer 함수의 렉시컬 환경을 가비지 컬렉션(Garbage Collection) 대상에서 제외하고 메모리에 남겨둔다.

그리고 반환된 innerFunc는 자신의 [[Environment]]라는 내부 슬롯에 자신이 태어난 환경, 즉 outer 함수의 렉시컬 환경에 대한 참조를 저장하고 있다. 따라서 innerFunc가 나중에 어디서 호출되든, 이 참조를 통해 outerVar에 접근할 수 있는 것이다. 이것이 바로 클로저의 핵심 동작 원리다.

3. 클로저 활용법 실전 예제로 마스터하기

클로저는 단순히 이론적인 개념에 그치지 않고, 실용적인 프로그래밍 패턴에서 매우 유용하게 사용된다.

3.1. 상태 은닉과 비공개 변수 (Private Variables)

가장 대표적인 클로저의 활용 사례는 상태를 안전하게 유지하고 외부로부터의 접근을 제어하는 것이다. 자바스크립트는 기본적으로 클래스 기반 언어의 private 키워드 같은 접근 제어자를 제공하지 않는다. 하지만 클로저를 사용하면 이를 흉내 낼 수 있다.

JavaScript

function createCounter() {
  let count = 0; // 비공개 변수

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

const counter1 = createCounter();
counter1.increase();
counter1.increase();
console.log(counter1.getCount()); // 2

const counter2 = createCounter();
console.log(counter2.getCount()); // 0

// 외부에서 count 변수에 직접 접근은 불가능하다.
console.log(counter1.count); // undefined

createCounter 함수는 count라는 변수와 이를 조작하는 세 개의 메서드( increase, decrease, getCount)를 포함하는 객체를 반환한다. count 변수는 createCounter 함수의 렉시컬 환경 내에 존재하며, 반환된 객체의 메서드들만이 이 count 변수에 접근할 수 있는 클로저가 된다. 따라서 외부에서는 count 변수에 직접 접근하거나 수정할 수 없게 되어, 상태가 의도치 않게 변경되는 것을 막을 수 있다. counter1counter2는 각각 독립적인 count 변수를 가진 별개의 클로저 환경을 갖게 된다.

3.2. 고차 함수(Higher-Order Function)와의 결합

클로저는 함수를 인자로 받거나 반환하는 고차 함수와 함께 사용될 때 강력한 시너지를 발휘한다.

JavaScript

// 특정 값으로 초기화하고, 그 값에 더하는 함수를 생성하는 함수
function createAdder(x) {
  return function(y) {
    return x + y;
  };
}

const add5 = createAdder(5); // x가 5인 클로저 함수 생성
const add10 = createAdder(10); // x가 10인 클로저 함수 생성

console.log(add5(2));  // 7 (5 + 2)
console.log(add10(2)); // 12 (10 + 2)

createAdder 함수는 숫자 x를 인자로 받아, y를 인자로 받는 새로운 함수를 반환한다. 이때 반환된 내부 함수는 x의 값을 기억하는 클로저다. add5x가 5로 고정된 버전의 덧셈 함수가 되고, add10x가 10으로 고정된 버전이 된다. 이처럼 클로저를 사용하면 함수의 일부 인자를 미리 고정시켜 재사용성을 높이는 **커링(Currying)**과 같은 함수형 프로그래밍 기법을 쉽게 구현할 수 있다.

3.3. 이벤트 핸들러와 콜백에서의 활용

클로저는 비동기적으로 처리되는 이벤트 핸들러나 콜백 함수에서도 매우 유용하다.

JavaScript

function setupEventListeners() {
  for (var i = 1; i <= 3; i++) {
    document.getElementById('button' + i).onclick = function() {
      console.log('You clicked button #' + i);
    };
  }
}
setupEventListeners();

위 코드는 많은 초보 개발자들이 실수하는 대표적인 예시다. 버튼 1, 2, 3 중 어느 것을 클릭해도 콘솔에는 “You clicked button #4”가 출력될 것이다. var로 선언된 변수 i는 함수 스코프를 가지므로, 반복문이 끝난 후의 최종 값인 4를 모든 onclick 핸들러가 공유하기 때문이다.

이 문제를 클로저를 사용해 해결할 수 있다.

JavaScript

function setupEventListenersWithClosure() {
  for (var i = 1; i <= 3; i++) {
    (function(savedI) { // 즉시 실행 함수(IIFE)로 새로운 스코프 생성
      document.getElementById('button' + i).onclick = function() {
        console.log('You clicked button #' + savedI);
      };
    })(i); // 반복문의 현재 i 값을 savedI에 복사
  }
}

또는 ES6의 let을 사용하면 훨씬 간단하게 해결된다. let은 블록 스코프를 가지므로, 반복문의 각 이터레이션마다 새로운 i 변수가 생성되어 클로저가 의도한 대로 동작하게 된다.

JavaScript

function setupEventListenersWithLet() {
  for (let i = 1; i <= 3; i++) {
    document.getElementById('button' + i).onclick = function() {
      console.log('You clicked button #' + i);
    };
  }
}

4. 심화 내용 클로저와 메모리 관리

클로저는 강력한 도구이지만, 잘못 사용하면 메모리 누수(Memory Leak)의 원인이 될 수 있다. 클로저는 상위 스코프의 변수에 대한 참조를 유지하기 때문에, 가비지 컬렉터가 해당 변수들이 차지하는 메모리를 회수하지 못하게 만든다.

만약 클로저가 더 이상 필요 없게 되었음에도 불구하고 어딘가에서 계속 참조되고 있다면, 클로저가 기억하는 외부 환경의 변수들 또한 메모리에서 해제되지 않는다.

JavaScript

function heavyTask() {
  const largeData = new Array(1000000).fill('some data'); // 큰 데이터

  // 이 함수가 largeData를 참조하는 클로저
  const getLargeData = function() {
    return largeData;
  };

  // 어딘가에 이 클로저를 등록했다고 가정
  // someGlobalObject.register(getLargeData);

  return function() {
    // getLargeData가 필요 없어져도, 참조가 남아있으면
    // largeData는 메모리에서 해제되지 않는다.
  };
}

이러한 메모리 누수를 방지하기 위해서는 클로저의 사용이 끝났을 때, 해당 클로저에 대한 참조를 명시적으로 제거해주는 것이 좋다.

JavaScript

let myClosure = createClosure();
// ... myClosure 사용 ...

// 더 이상 사용하지 않을 때
myClosure = null; // 참조를 끊어 가비지 컬렉터가 수집해가도록 유도

5. 결론 클로저, 유능한 개발자의 증표

클로저는 자바스크립트의 단순한 기능이 아니라, 언어의 핵심 철학과 동작 방식을 이해하는 척도다. 렉시컬 스코프의 개념 위에서, 함수가 자신의 탄생 환경을 기억하고 그 환경과 상호작용할 수 있도록 만드는 메커니즘인 클로저는 다음과 같은 강력한 이점을 제공한다.

장점설명
상태 은닉외부에서 접근할 수 없는 비공개 변수를 만들어 데이터의 무결성을 보장한다.
모듈화특정 상태와 그를 조작하는 함수들을 하나의 단위로 묶어 코드의 재사용성과 유지보수성을 높인다.
지연 실행함수의 실행 시점을 제어하고, 필요한 시점에 특정 컨텍스트의 데이터를 사용할 수 있게 한다.
함수형 프로그래밍커링, 고차 함수 등 함수형 프로그래밍 패러다임을 자바스크립트에서 자연스럽게 구현하도록 돕는다.

클로저를 깊이 이해하고 자유자재로 활용할 수 있게 되면, 당신은 더 이상 단순히 코드를 작성하는 사람이 아니라, 데이터의 흐름과 상태를 정교하게 설계하는 아키텍트의 관점을 갖게 될 것이다. 이 핸드북이 당신의 자바스크립트 여정에 든든한 길잡이가 되기를 바란다. 이제 클로저의 힘을 빌려, 더욱 견고하고 우아한 코드를 만들어보자.