Menu

자바스크립트 클로저(Closure) 개념과 활용법 정리

클로저는 자신이 선언된 스코프의 변수를 기억하는 함수입니다. 실행 가능한 예제와 실제 사용 사례로 자바스크립트 클로저의 동작 원리를 살펴봅니다.

기억하는 함수, 클로저란?

자바스크립트에서 함수를 정의할 때마다, 그 함수는 주변에 있던 변수들과의 연결고리를 조용히 간직합니다. 나중에 그 함수가 전혀 다른 곳에서 실행되더라도 그 변수들을 여전히 들여다볼 수 있죠. 이게 바로 자바스크립트 클로저(closure) 입니다.

가장 짧은 예제로 살펴볼게요:

index.js
Output
Click Run to see the output here.

makeGreeter가 실행되면 내부 함수를 반환하고 종료됩니다. 상식적으로 생각하면 지역 변수 name은 함수가 끝나는 순간 사라져야겠죠. 그런데 반환된 내부 함수가 아직 name을 참조하고 있기 때문에, 자바스크립트는 이 변수를 살려둡니다. 그래서 greetAda"Ada"를, greetBoris"Boris"를 각각 기억하게 되죠. 클로저가 두 개 만들어졌고, 저장된 값도 따로따로입니다.

클로저의 뿌리, 렉시컬 스코프

클로저를 가능하게 하는 규칙을 렉시컬 스코프(lexical scope) 라고 부릅니다. 함수는 호출된 위치가 아니라 작성된 위치의 변수를 볼 수 있다는 뜻이에요. "렉시컬"이라는 말 자체가 "소스 코드상 어디에 쓰여 있느냐를 기준으로 한다"는 의미입니다.

index.js
Output
Click Run to see the output here.

show가 찍는 값은 "나는 바깥에 있어요""나는 caller 안에 있어요"가 아닙니다. 함수가 작성된 위치가 최상위 outer 옆이니까, 그 outer를 참조하는 거죠. 호출하는 쪽에 우연히 같은 이름의 outer가 있든 말든 상관없습니다.

결국 자바스크립트 클로저라는 건, 외부 함수가 끝난 뒤에도 살아남는 렉시컬 스코프일 뿐입니다. 누군가 여전히 그 변수를 참조하고 있으니 사라지지 않는 거죠.

호출할 때마다 새로운 클로저가 만들어진다

외부 함수를 다시 호출하면 변수도 새로 만들어지고, 그 호출에서 반환된 내부 함수는 바로 그 변수들을 기억합니다. 앞서 본 greetAdagreetBoris가 서로 간섭하지 않았던 이유가 바로 이것입니다.

대표적인 예가 카운터입니다:

index.js
Output
Click Run to see the output here.

ab는 각자 자기만의 count를 들고 있습니다. 반환된 함수 바깥에서는 이 변수에 절대 손댈 수 없죠. count는 완전히 private입니다. 우리가 따로 켜준 언어 기능이 아니라, 클로저가 동작하는 방식에서 자연스럽게 흘러나오는 결과입니다.

클래스 없이 private 변수 만들기

감싸인 변수는 반환된 함수를 통해서만 접근할 수 있기 때문에, 자바스크립트 클로저를 활용하면 진짜로 private한 상태를 가진 작은 객체를 손쉽게 만들 수 있습니다.

index.js
Output
Click Run to see the output here.

balance는 반환된 객체의 프로퍼티가 아닙니다. 클로저 안에 숨어 있죠. 이 값을 읽거나 바꾸려면 오직 밖으로 노출한 메서드를 통해서만 가능합니다. 요즘은 클래스의 #private 필드로도 비슷하게 구현할 수 있지만, 클로저 방식은 그보다 수십 년 앞서 등장했고 지금도 자바스크립트 생태계 곳곳에서 흔히 볼 수 있습니다.

반복문에서 자주 마주치는 클로저 함정

자바스크립트 클로저 때문에 가장 많이들 헷갈리는 상황이 바로 반복문 안입니다. var를 쓰면 어떤 일이 벌어지는지 한번 봅시다:

index.js
Output
Click Run to see the output here.

0, 1, 2가 찍힐 거라 기대하지만 실제로는 3, 3, 3이 나옵니다. 왜 그럴까요? var는 함수 스코프라서 반복문 전체에서 i하나만 존재합니다. 세 클로저가 모두 같은 변수를 참조하게 되고, 막상 실행될 시점엔 반복문이 이미 끝나서 i3인 상태죠.

let으로 바꿔봅시다:

index.js
Output
Click Run to see the output here.

이제 0, 1, 2가 찍힙니다. let은 블록 스코프라서, 반복문이 돌 때마다 i가 새롭게 바인딩됩니다. 그래서 각 클로저가 자기만의 값을 품게 되는 거죠. 반복문에서 var 대신 let을 써야 하는 가장 큰 이유가 바로 이겁니다.

클로저는 값이 아니라 변수를 캡처한다

살짝 까다롭지만 꼭 짚고 넘어가야 할 포인트가 있습니다. 클로저는 함수가 정의된 시점의 값 스냅샷 을 붙잡는 게 아니라, 변수 자체 를 붙잡습니다.

index.js
Output
Click Run to see the output here.

printMessage는 함수가 만들어질 때가 아니라 실행되는 시점message 값을 읽습니다. 만약 그 순간의 값을 스냅샷처럼 박제하고 싶다면, 먼저 지역 변수에 값을 복사해 두면 됩니다. 사실 for 반복문 안에서 let이 하는 일이 바로 이거예요.

실전에서 자주 쓰이는 패턴: Once 함수

클로저를 활용해 함수가 딱 한 번만 실행되도록 만드는 작은 유틸리티를 살펴볼게요.

index.js
Output
Click Run to see the output here.

calledresult는 반환된 함수가 살아 있는 동안만 유지되는 private 상태입니다. 전역 플래그도, 별도의 객체도 필요 없죠. 작은 헬퍼 함수에 private 상태를 담아 클로저로 감싸는 이 패턴은 자바스크립트가 제공하는 가장 유용한 도구 중 하나입니다.

메모리에 관한 이야기

클로저는 자신을 참조하는 무언가가 남아 있는 한, 캡처한 변수들을 계속 살려 둡니다. 대부분의 경우 우리가 원하는 동작이지만, 수명이 긴 대상(예: DOM 이벤트 리스너나 전역 캐시)에 클로저를 붙였는데 그 클로저가 덩치 큰 값을 캡처하고 있다면 주의해야 합니다. 클로저가 사라지기 전까지는 그 값도 가비지 컬렉션 대상이 되지 못하거든요.

function attach() {
    const hugeData = new Array(1_000_000).fill("...");
    document.addEventListener("click", () => {
        console.log(hugeData.length);
    });
}

리스너가 붙어 있는 동안에는 hugeData도 메모리에 계속 남습니다. 리스너를 제거하거나 애초에 필요 없는 값은 캡처하지 않으면 참조가 풀리죠. 이 부분을 일일이 신경 쓸 필요까지는 없지만, 클로저와 메모리가 맞닿아 있다는 사실만큼은 기억해 두면 좋습니다.

핵심 정리

  • 클로저란 함수와, 그 함수가 정의될 때 볼 수 있던 변수들을 묶어 놓은 것입니다.
  • 외부 함수를 호출할 때마다 내부 클로저가 쓸 변수 세트가 새로 만들어집니다.
  • 클로저를 활용하면 클래스 없이도 private 상태를 만들 수 있습니다.
  • 반복문 안에서는 let을 써서 매 반복마다 독립된 바인딩을 갖게 하세요.
  • 클로저는 값이 아니라 변수 자체를 캡처합니다.

다음 주제: this 키워드

클로저가 함수 주변의 변수를 다루는 장치라면, 다음으로 살펴볼 것은 그 함수가 어떤 대상 위에서 호출되는가 입니다. 자바스크립트에서는 이걸 this가 결정하는데, 방금 다룬 캡처된 변수와는 완전히 다른 방식으로 동작합니다.

자주 묻는 질문

자바스크립트에서 클로저란 무엇인가요?

클로저는 자신이 정의된 스코프의 변수를 기억하는 함수입니다. 바깥 스코프의 실행이 끝난 뒤에도 그 변수들을 계속 참조할 수 있죠. 사실 자바스크립트의 모든 함수는 기술적으로 클로저지만, 보통은 함수가 return되거나 다른 곳으로 전달된 뒤에도 원래 스코프의 변수를 그대로 쓰는 상황을 가리킬 때 이 용어를 씁니다.

클로저는 왜 유용한가요?

함수가 자신만의 상태(state)를 들고 다닐 수 있게 해주기 때문입니다. 클래스나 전역 변수를 쓰지 않고도 함수에 데이터를 묶어둘 수 있죠. 대표적인 활용 예로는 카운터, 한 번만 실행되는 콜백, 메모이제이션 헬퍼, 그리고 내부 구현을 감춘 작은 API를 만들 때 등이 있습니다.

var로 만든 반복문에서 클로저가 이상하게 동작하는 이유는 뭔가요?

var는 함수 스코프라서 반복문의 모든 순회가 같은 변수를 공유합니다. 반복문 안에서 만들어진 클로저들이 전부 그 하나의 변수를 참조하는데, 실제로 실행되는 시점에는 이미 마지막 값으로 바뀌어 있죠. 이럴 땐 let을 쓰면 됩니다. 블록 스코프라서 매 순회마다 새로운 바인딩이 만들어지거든요.

Coddy로 코딩 배우기

시작하기