본문 바로가기
코드의 해부학

클로저: 죽지 않는 변수들의 이야기

by Zev 2025. 6. 1.
728x90
반응형

클로저(Closure)란 무엇인가?

클로저는 자바스크립트의 가장 강력하고도 때로는 혼란스러운 개념 중 하나입니다. 단순히 함수와 변수의 묶음을 넘어, 함수가 자신이 선언된 특정 환경(어휘적 환경)을 "기억"하고, 그 환경 속의 변수들에 접근할 수 있도록 하는 특별한 메커니즘입니다. 마치 옛 친구가 시간이 한참 흐른 뒤에도 당신의 옛날 이야기를 기억하고 꺼내는 것처럼, 함수가 자신을 감싸고 있던 과거의 스코프를 잊지 않고 계속해서 활용하는 자바스크립트의 놀라운 능력이라고 할 수 있죠.

자바스크립트에서 **함수는 일급 객체(First-class object)**입니다. 이 말은 함수를 변수에 할당하거나, 다른 함수의 인자로 전달하거나, 함수에서 반환할 수 있다는 의미입니다. 이러한 유연성 덕분에 클로저라는 개념이 자연스럽게 발생하게 됩니다.


클로저 작동의 핵심: 어휘적 환경(Lexical Environment)

클로저를 제대로 이해하려면, 먼저 **어휘적 환경(Lexical Environment)**이라는 개념을 확실히 파악해야 합니다. 이는 클로저의 모든 마법이 시작되는 지점입니다.

스코프의 청사진, 코드의 설계도

어휘적 환경은 변수가 코드 상에서 '어디에' 선언되었는지를 기준으로, 해당 변수에 접근할 수 있는 범위를 구조적으로 기록한 스코프의 내부적인 구조입니다. 이 환경은 코드를 작성하는 순간(파싱 시점) 결정되며, 런타임에 동적으로 변경되지 않습니다. 이것이 바로 '어휘적(Lexical)'이라는 이름이 붙은 이유입니다. 코드를 읽는 '어휘' 단계에서 이미 스코프가 정해진다는 뜻이죠.

모든 어휘적 환경은 두 가지 핵심 구성 요소를 가집니다:

  • 환경 레코드(Environment Record): 현재 실행 중인 스코프(예: 특정 함수 스코프, 블록 스코프) 내에서 선언된 모든 식별자(변수, 함수 선언, 함수 매개변수 등)를 저장하는 공간입니다. 예를 들어, let userName = 'Alice';와 같은 선언은 환경 레코드에 userName과 그 값 'Alice'를 기록합니다.
  • 외부 참조(Outer Environment Reference): 이 부분이 클로저를 이해하는 데 가장 중요합니다. 현재 어휘적 환경이 생성될 당시의 상위 스코프, 즉 자신을 감싸는 외부 함수의 어휘적 환경을 가리키는 포인터입니다. 자바스크립트 엔진은 이 '외부 참조'를 통해 스코프 체인을 따라 변수를 찾아 올라가면서 변수의 값을 확인하고 할당합니다. 마치 도서관에서 책을 찾을 때, 현재 위치에서 책이 없으면 옆 서가로 이동하고, 거기에도 없으면 다음 서가로 이동하는 것과 비슷합니다.
JavaScript
 
function outer() { // outer 함수의 어휘적 환경이 생성됩니다.
  let outerVar = '나는 바깥 변수야'; // outerVar는 outer 함수의 환경 레코드에 저장됩니다.

  function inner() { // inner 함수의 어휘적 환경이 생성됩니다.
    // inner 함수의 '외부 참조'는 outer 함수의 어휘적 환경을 가리킵니다.
    console.log(outerVar);
  }

  return inner; // inner 함수 자체를 반환합니다.
}

const closureFunc = outer(); // outer 함수가 실행되고, inner 함수가 반환됩니다.
// 여기서 중요! outer 함수는 실행을 마쳤고, 그 실행 컨텍스트는 스택에서 제거됩니다.
// 하지만 inner 함수는 여전히 outerVar에 대한 참조를 '기억'하고 있습니다.

closureFunc(); // "나는 바깥 변수야" 출력
// inner 함수가 호출될 때, 자신의 환경 레코드에서 outerVar를 찾지만 없습니다.
// 그러면 '외부 참조'를 따라 outer 함수의 환경 레코드에서 outerVar를 찾아 값을 가져옵니다.
// 이것이 바로 클로저의 마법입니다. inner 함수는 선언 당시의 어휘적 환경을 '기억'하기 때문에,
// outer 함수가 실행 스택에서 사라졌어도 outerVar에 성공적으로 접근할 수 있는 것입니다.

클로저의 탄생 조건: 스코프의 영원한 생명 연장

클로저는 자바스크립트 코드를 작성하는 과정에서 특정 조건이 충족되면 자동으로 형성됩니다. 이는 자바스크립트 엔진의 가비지 컬렉터(Garbage Collector, GC) 작동 방식과 긴밀하게 연결되어 있죠.

스코프를 붙잡는 '끈'

핵심 조건은 단 하나입니다: 내부 함수(혹은 반환된 함수)가 외부 함수의 스코프에 있는 변수를 참조할 때입니다. 이때 외부 함수가 실행을 마치고 메모리에서 사라져야 할 시점에도, 내부 함수가 해당 변수를 '붙잡고' 있기 때문에 클로저가 형성됩니다.

JS 엔진의 현명한 판단: 사라지지 않는 기억

일반적으로 함수 실행이 끝나면 해당 함수의 지역 변수들은 더 이상 필요 없다고 판단되어 메모리에서 해제되고 가비지 컬렉션의 대상이 됩니다. 하지만 클로저의 경우에는 다릅니다. 자바스크립트 엔진은 어떤 변수가 다른 함수(즉, 내부 함수)에 의해 여전히 '참조'되고 있다면, 해당 변수가 포함된 어휘적 환경 전체를 가비지 컬렉션 대상에서 제외시킵니다.

이러한 동작은 참조 중인 변수가 갑자기 사라져 발생하는 치명적인 오류를 방지하기 위함입니다. 결과적으로, 클로저를 통해 외부 스코프의 변수들은 외부 함수가 '죽었음'에도 불구하고 '죽지 않고' 메모리에 살아남아 내부 함수에 의해 지속적으로 접근되고 조작될 수 있게 됩니다. 이것이 바로 "죽은 함수의 망령"이라는 은유가 들어맞는 지점입니다.


클로저, 언제 활용될까? 실용적인 슈퍼 파워

클로저는 단순히 이론적인 개념을 넘어, 실제 자바스크립트 개발에서 매우 강력하고 유연한 디자인 패턴으로 활용됩니다. 마치 다재다능한 만능 도구처럼 다양한 문제 해결에 기여하죠.

(1) 정보 은닉(Information Hiding) 및 상태 유지(State Preservation): 캡슐화의 마법

클로저의 가장 대표적인 사용 사례입니다. 특정 변수를 외부 코드에서 직접 접근하지 못하게 숨기고, 오직 클로저를 통해 반환된 함수를 통해서만 접근하고 조작하게 함으로써 데이터의 무결성을 보호할 수 있습니다. 이는 객체 지향 프로그래밍의 캡슐화 개념과 매우 유사한 효과를 제공합니다.

JavaScript
 
function createCounter() {
  let count = 0; // 이 변수는 createCounter 함수의 스코프 안에 숨겨져 있습니다.
                  // 외부에서는 직접 count에 접근할 수 없어요.

  return function increment() { // 이 내부 함수가 클로저입니다.
    count++; // increment 함수는 count에 접근하여 값을 증가시킵니다.
    console.log(count);
  };
}

const counter1 = createCounter(); // createCounter() 실행 후, count는 메모리에 남아 있습니다.
                                 // counter1은 이제 자신만의 독립적인 count를 가진 클로저입니다.
counter1(); // 1 (count는 메모리에 남아 있으며, counter1에 의해 관리됩니다)
counter1(); // 2 (동일한 count 변수를 계속 참조합니다)

const counter2 = createCounter(); // 새로운 클로저, 자신만의 독립적인 count 변수를 가집니다.
counter2(); // 1 (counter1의 count와는 완전히 다른 count 변수입니다)
// `count` 변수는 `createCounter` 함수가 종료되어도 메모리에 남아 있으며,
// 클로저를 통해 **비공개(private) 상태**로 지속적으로 접근 및 변경 가능합니다.

(2) 부분 적용(Partial Application) 및 커링(Currying): 함수 재활용의 달인

여러 인자를 받는 함수를 특정 인자들을 미리 고정하여 새로운 함수를 생성할 때 클로저가 매우 유용하게 활용됩니다. 이는 함수형 프로그래밍에서 코드를 더욱 유연하고 재사용 가능하게 만드는 중요한 기법입니다.

JavaScript
 
function multiplier(factor) { // factor 인자가 외부 스코프에 정의됩니다.
  return function (number) { // 이 내부 함수는 factor를 기억합니다.
    return number * factor;
  };
}

const double = multiplier(2); // multiplier(2) 실행 후 factor는 클로저에 '2'로 묶입니다.
const triple = multiplier(3); // multiplier(3) 실행 후 factor는 클로저에 '3'으로 묶입니다.

console.log(double(5)); // 10 (double은 항상 2를 곱합니다)
console.log(triple(5)); // 15 (triple은 항상 3을 곱합니다)
// `factor` 변수는 외부 함수에서 선언됐지만, 내부 함수에서 계속 참조되어
// 마치 고정된 인자처럼 작동하며 새로운 함수를 만들어냅니다.

(3) 콜백 함수에서 외부 변수 참조: 비동기 처리의 든든한 조력자

웹 애플리케이션에서 흔히 사용되는 비동기 처리(setTimeout, setInterval, Promise 등)나 이벤트 핸들링 (addEventListener)에서 콜백 함수가 외부 함수의 변수에 접근해야 할 때 클로저가 자연스럽게 형성됩니다.

JavaScript
 
function setupButtonMessage(message) {
  document.getElementById("myButton").addEventListener("click", function () {
    // 이 익명 함수(콜백 함수)는 setupButtonMessage의 어휘적 환경에 '닫혀(closed over)' 있습니다.
    alert(message); // message는 setupButtonMessage 함수가 끝났어도 콜백에 의해 참조됩니다.
  });
}

// setupButtonMessage("버튼이 클릭되었습니다!");
// 이 `setupButtonMessage` 함수는 페이지 로드 시 단 한 번 실행됩니다.
// 그러나 그 안에 정의된 이벤트 리스너는 사용자가 `myButton`을 클릭할 때마다 호출되며,
// 그때마다 `setupButtonMessage` 함수 스코프의 `message` 변수에 접근하여 알림을 띄울 수 있습니다.
// `message` 변수가 사라지지 않고 콜백이 호출될 때까지 유지되는 것이 클로저 덕분입니다.

클로저 사용 시 주의점: 강력함 뒤에 숨겨진 함정

클로저는 강력한 도구이지만, 부주의하게 사용하면 예상치 못한 문제로 이어질 수 있습니다. 마치 날카로운 칼처럼, 유용하지만 조심해서 다뤄야 하죠.

(1) 메모리 누수(Memory Leak)의 위험

클로저가 외부 스코프의 변수를 참조하는 한, 해당 변수는 가비지 컬렉션의 대상에서 제외됩니다. 만약 클로저가 불필요하게 오랫동안 유지되거나, 클로저가 참조하는 변수가 이미지, 대규모 데이터 배열 등 매우 크다면 메모리 누수가 발생하여 애플리케이션의 성능 저하로 이어질 수 있습니다. 특히 DOM 요소에 대한 참조를 클로저가 계속 유지하는 경우, 해당 DOM 요소가 페이지에서 제거되어도 메모리에서 해제되지 않을 수 있으므로 주의해야 합니다.

(2) 의도치 않은 참조(Closures in Loops): var의 함정

var 키워드를 사용하여 루프 내에서 클로저를 생성할 때 매우 흔하게 발생하는 문제입니다. var는 함수 스코프를 따르기 때문에, 루프가 종료된 후 모든 클로저가 동일한 i 변수의 최종 값을 참조하게 됩니다.

JavaScript
 
for (var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i); // 예상: 0, 1, 2
                  // 실제: 3, 3, 3 (setTimeout 실행 시점에는 이미 루프가 끝나 i가 3으로 변경되어 있습니다)
  }, 1000);
}

// ➡ `let`을 사용하면 블록 스코프를 가지므로 의도대로 작동합니다:
for (let i = 0; i < 3; i++) { // 각 반복마다 새로운 'i' 변수가 생성됩니다.
  setTimeout(function () {
    console.log(i); // 0, 1, 2 (각 반복마다 생성된 새로운 'i' 변수가 클로저에 캡처됩니다)
  }, 1000);
}

이 문제를 해결하기 위해서는, 각 반복마다 고유한 스코프를 생성하는 let 키워드를 사용하거나, 즉시 실행 함수 표현(IIFE)을 활용하여 각 반복마다 새로운 i 값을 클로저에 캡처해 주어야 합니다.


 

클로저, 자바스크립트 개발자의 필수 지식

항목설명
정의 함수와 그 함수가 선언 당시의 어휘적 환경(스코프)의 조합
핵심 역할 외부 스코프의 변수에 접근하고 유지할 수 있는 내부 함수를 생성
주의점 불필요한 참조로 인한 메모리 누수 가능성, 루프 내 var 사용 시 의도치 않은 참조 주의
주요 장점 정보 은닉(캡슐화) 통한 데이터 보호, 특정 상태 보존, 함수형 프로그래밍 패턴 구현 용이

 


마무리하며: 클로저, 코드에 생명을 불어넣다

클로저는 자바스크립트의 가장 근본적인 특징 중 하나이며, 스코프, 함수, 그리고 메모리 관리 구조를 깊이 이해하는 데 필수적인 개념입니다. 단순히 몇 가지 예시를 암기하는 것을 넘어, 클로저가 왜 발생하며, 어떤 상황에서 어떤 이점과 주의점을 가지는지를 파악하는 것이 중요합니다. 

728x90
반응형