10분 만에 이해하는 자바스크립트 ‘클로저(Closure)’
클로저(Closure)는 자바스크립트의 핵심 개념 중 하나이지만, 개발자들이 흔히 어렵게 느끼는 개념이기도 합니다. 공식 문서나 책을 봐도 “클로저는 함수와 렉시컬 환경의 조합입니다”와 같은 문장이 나오는데, 바로 이해가 되지 않아 머리가 아파오기도 하죠. 하지만 사실 클로저는 이해하기 쉬운 개념 중 하나입니다. 단지 설명이 어렵게 느껴졌을 뿐이죠.
이번 글에서는 클로저의 정확한 정의부터 예제 코드를 통한 구체적인 분석까지 하나씩 천천히 풀어서 알려드리겠습니다. 단, 클로저를 이해하려면 실행 컨텍스트와 렉시컬 환경이라는 개념이 중요한데요. 혹시 실행 컨텍스트라는 개념이 낯설다면, 이전 글 ‘자바스크립트 코드는 어떻게 실행될까?’를 먼저 읽고 오시면 좋습니다.
클로저란?
클로저의 정의를 인터넷에 검색해 보면, 다음과 같은 설명이 나옵니다.
“클로저는 주변 상태에 대한 참조와 함께 묶인 함수의 조합입니다. 즉, 클로저는 내부 함수에서 외부 함수의 범위에 대한 접근을 제공합니다.”
1) 클로저의 정의
사실 이런 설명을 처음 접하면 대부분 혼란스러울 것 같은데요, ‘주변 상태’, ‘참조’, ‘범위에 대한 접근’과 같은 용어들은 마치 수학이나 철학책에서 나오는 어려운 말들처럼 들리기도 합니다. 이렇게 복잡하게 느껴지는 이유는, 클로저를 한 번에 너무 많은 개념을 담아 설명하려고 하기 때문입니다.
하지만 클로저는 사실 매우 직관적이고 간단한 개념입니다. 내부 함수가 자신이 선언된 환경, 즉 외부 함수의 변수나 상태를 기억하고 있다가, 나중에 그 환경에 계속 접근하고 사용할 수 있다는 것이 핵심 개념인데요. 이 글을 끝까지 읽고 나면, 위의 복잡한 공식 같은 정의가 얼마나 쉽고 간단한 개념을 어렵게 표현한 것인지 깨닫게 될 것입니다.
2) 예제 코드 분석
그렇다면, 다음 예제 코드를 통해 클로저가 어떻게 작동하는지 하나씩 자세하게 살펴보겠습니다.
function outer() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const counter = outer();
counter(); // 1
counter(); // 2
</코드>
위의 코드에서 무슨 일이 일어나는지 차근차근 분석해 볼게요. 먼저 outer라는 외부 함수가 정의되어 있습니다. 이 함수 안에는 count라는 변수가 존재합니다. outer 함수는 익명 함수를 반환하고, 이 내부 함수에서는 외부에 정의된 변수 count를 사용해 값을 증가시키고, 증가된 값을 출력합니다.
이제 outer() 함수를 호출하면, 반환된 내부 함수를 counter라는 변수에 저장합니다. 이때 단순하게 생각해 보면, 이미 outer 함수의 실행이 종료되었기 때문에 outer 함수 내부의 count 변수 또한 사라졌다고 생각하게 되는데요. 내부 함수는 ‘클로저’의 특성 덕분에 외부 함수의 환경을 기억하고 있습니다. 즉, 내부 함수가 여전히 count 변수를 참조하고 있는 것입니다.
따라서 이후에 counter()를 호출할 때마다, 내부 함수는 여전히 살아있는 count 변수를 찾아 값을 증가시키고 출력할 수 있게 됩니다. 이처럼 내부 함수가 외부 함수의 환경을 기억하는 능력이 클로저의 핵심적인 원리입니다. 덕분에 외부 함수가 종료된 이후에도, 그 내부 상태(변수)에 지속적으로 접근하고 관리할 수 있습니다.
그림으로 이해하면 조금 더 쉽게 이해할 수 있는데요. outer 함수 실행 컨텍스트가 생성되었을 때, 콜스택 내부를 그림으로 나타내 보면 다음과 같습니다.

outer 함수 실행 컨텍스트 내부에서는 지역 변수인 count가 선언되고 0으로 초기화되며, 함수 내부의 익명 함수가 반환됩니다. 여기서 반환된 익명 함수는 counter 변수에 저장되면서, 외부의 count 변수(외부 렉시컬 환경)를 클로저로 유지하게 됩니다.
그다음, outer 함수가 종료되어 outer 함수 실행 컨텍스트가 종료된 이후 counter() 코드가 실행되면, counter 변수에는 아까 반환된 익명 함수가 저장되어 있기 때문에 다시 익명 함수가 호출되어, 익명 함수 실행 컨텍스트가 생성됩니다. 그림으로 나타내면 아래와 같습니다.

counter 함수가 호출되면, 익명 함수 실행 컨텍스트가 만들어지고, 이 함수는 자신의 내부 슬롯에 저장된 외부 렉시컬 환경을 참조합니다. 여기서 외부 렉시컬 환경은 outer 함수의 환경이며, 여기에는 지역 변수인 count가 존재하죠.
내부 함수는 바로 이 내부 슬롯을 통해, outer 함수의 지역 변수인 count를 기억하고 접근할 수 있는 것입니다. 따라서 이 함수가 실행되면서 외부 함수(outer)의 지역변수인 count를 1 증가시켜 1을 출력하게 됩니다.
예제 코드를 통한 클로저 학습
이제 클로저를 더 자세히 알아보기 위해 구체적인 예제 코드를 분석해 보겠습니다. 클로저가 동작하는 원리를 이해하려면, 두 가지 개념을 명확하게 알아야 하는데요. 두 가지 개념이 바로 렉시컬 스코프와 내부 슬롯입니다. 위에서 간단하게 언급했지만, 중요한 개념이기 때문에 한 번 더 다뤄볼게요.
1) 렉시컬 스코프와 내부 슬롯
자바스크립트는 변수를 참조할 때, 변수가 어디에서 선언되었는지를 기준으로 그 변수를 찾는데요, 이것을 바로 렉시컬 스코프라고 부릅니다. 쉽게 말해보자면, 함수가 어디에서 선언되었는지에 따라 접근할 수 있는 변수의 범위가 결정된다는 뜻입니다.
좀 더 구체적으로 살펴볼까요? 렉시컬 스코프는 변수가 선언된 위치에 따라 스코프 체인(Scope Chain)이 만들어지는 원리입니다. 자바스크립트 엔진은 함수가 선언될 때, 그 함수가 선언된 위치를 기준으로 스코프 체인을 만듭니다. 이후 해당 함수가 실행될 때, 자바스크립트 엔진은 이 스코프 체인을 따라 변수를 찾아갑니다.

여기서 중요한 역할을 하는 것이 바로 내부 슬롯([[Environment]])입니다. 내부 슬롯은 함수가 생성될 때 만들어지는 특별한 저장 공간으로, 함수가 선언된 환경의 정보를 담고 있습니다. 내부 함수는 이 내부 슬롯을 이용해, 외부 함수의 변수와 환경을 기억하고 접근할 수 있게 되는 것입니다.
다음 예제를 통해 한 번 더 자세히 살펴볼까요?
let username = 'John';
function greetUser() {
const greeting = 'Hello';
return function() {
console.log(`${greeting}, ${username}`);
};
}
const greet = greetUser();
greet(); // Hello, John
이 코드에서 내부 함수는 두 가지 변수를 기억하고 있습니다. 첫 번째는 전역 스코프에 존재하는 username입니다. 내부 함수가 선언될 때 이미 이 변수는 전역 환경에 있었고, 내부 함수는 이를 기억하고 접근할 수 있습니다.
두 번째는 자신을 감싸고 있는 외부함수, greetUser 함수의 지역 변수 greeting입니다. 외부 함수가 종료된 후에도 내부 함수는 이 변수에 접근할 수 있게 됩니다. 내부 함수가 이 두 변수를 계속해서 사용할 수 있는 이유는 바로 내부 슬롯 덕분이며, 이것이 바로 클로저의 핵심적인 작동 원리입니다.
2) 클로저에 대한 새로운 정의
이제 클로저를 다시 쉽게 정의해 볼게요. 클로저란, “내부 함수가 외부 함수의 변수를 기억해, 외부 함수가 종료된 이후에도 계속 접근할 수 있는 함수”입니다. 내부 함수가 외부 환경의 변수를 유지하고, 관리할 수 있는 특별한 능력을 클로저라고 생각하면 더욱 이해하기 쉽습니다. 그래서 내부 함수가 외부 함수에 선언된 변수를 기억해, 외부 함수가 종료된 이후에도 그 변수에 계속해서 접근할 수 있도록 하는 자바스크립트의 특수한 기능입니다.
외부 함수가 실행되면서 내부 함수를 반환하면, 외부 함수는 종료되지만, 내부 함수는 외부 함수에서 선언한 변수와 그 변수가 속한 환경을 기억합니다. 이러한 환경 정보가 함수의 내부 슬롯([[Environment]])에 저장되고, 이를 통해 내부 함수는 이후 실행될 때도 외부 함수의 변수를 참조할 수 있게 됩니다.
쉽게 말해, 클로저는 내부 함수가 “자신이 태어난 환경을 절대 잊지 않고 계속 기억하는 능력”이라고 할 수 있습니다. 클로저 덕분에 자바스크립트는 매우 강력한 상태 관리나 캡슐화를 구현할 수 있죠. 이제 클로저가 어떻게 활용되는지, 이어서 살펴볼게요.
활용 사례
클로저는 자바스크립트에서 굉장히 유용하게 쓰이는데요. 대표적인 활용 사례를 통해 클로저에 대한 개념을 정리해 보겠습니다.
1) 상태 유지 함수
클로저가 가장 많이 활용되는 예는 바로 상태 유지입니다. 앞서 살펴본 개념을 활용한 대표적인 사례인데요, 상태를 유지하는 간단한 카운터 예제를 살펴봅시다.
function clickCounter() {
let clicks = 0;
return function() {
clicks += 1;
console.log(`클릭 횟수: ${clicks}`);
};
}
const counter = clickCounter();
counter(); // 클릭 횟수: 1
counter(); // 클릭 횟수: 2
counter(); // 클릭 횟수: 3
위 코드에서 내부 함수는 외부 함수의 clicks 변수를 클로저를 통해 계속 기억하고 유지할 수 있습니다. 내부 함수가 호출될 때마다 값이 증가하며 유지되는 것이 클로저 덕분입니다.
2) 데이터 은닉과 캡슐화
클로저는 데이터를 외부로부터 보호하고 안전하게 관리하는 데에도 매우 유용하게 쓰입니다. 데이터를 숨기고 접근을 제한하는 방법을 클로저로 구현할 수 있는데요, 다음 예제를 통해 살펴보겠습니다.
function createAccount(initialBalance) {
let balance = initialBalance; // 외부에서 접근 불가능한 프라이빗 변수
return {
deposit(amount) {
balance += amount;
console.log(`입금 후 잔액: ${balance}`);
},
withdraw(amount) {
if(amount > balance) {
console.log('잔액이 부족합니다.');
} else {
balance -= amount;
console.log(`출금 후 잔액: ${balance}`);
}
}
};
}
const myAccount = createAccount(1000);
myAccount.deposit(500); // 입금 후 잔액: 1500
myAccount.withdraw(300); // 출금 후 잔액: 1200
myAccount.withdraw(1300); // 잔액이 부족합니다.
여기서 balance라는 변수는 외부에서는 절대로 접근할 수 없고, 오직 반환된 메서드 deposit과 withdraw를 통해서만 수정할 수 있습니다. 이 변수를 보호할 수 있는 이유는, 내부 함수가 balance 변수를 클로저로 유지하고 있기 때문입니다. 이러한 방식을 통해 안전하게 데이터를 관리할 수 있게 됩니다.

마치며
지금까지 클로저(Closure)의 개념을 정의하고, 대표적인 활용 예제까지 함께 살펴보았습니다. 이 글을 통해 클로저가 더 이상 어렵고 낯선 개념이 아니라, 여러분의 프로그래밍 작업을 든든히 도와주는 믿음직한 무기가 될 수 있길 바랍니다.
©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.