Search

[모던 자바스크립트 Deep Dive] - 24 클로저

생성일
2025/05/27
URL
안녕하세요, 장동호입니다!
오늘은 클로저에 대해 이야기 해보려고 합니다.

클로저란?

클로저(Closure)는 함수가 선언될 때의 스코프(Lexical Scope)를 기억하는 함수입니다.
한 마디로, 자기 주변에 있는 변수들을 기억하고 있는 함수라고 생각하면 됩니다.
클로저가 왜 필요한지 사진관 비유를 통해 알아보겠습니다.
동호는 서울에 있는 유명한 사진관을 방문했습니다. 사진사는 동호의 사진을 찍고 나서, 앨범 속에 고이 저장해둡니다. 나중에 동호가 다시 방문하면, 사진사는 직접 앨범을 꺼내 사진을 보여줍니다.
여기서 중요한 점은:
사진은 사진사가 보관하고
고객(동호)은 직접 사진에 접근할 수 없고,
오직 사진사를 통해서만 사진을 볼 수 있어요.
이를 자바스크립트 코드로 표현하면 다음과 같습니다.
function 사진찍기(고객이름) { const 사진 = `${고객이름} 사진`; function 사진가져오기() { console.log(사진); } return 사진가져오기; } let 동호사진가져오기 = 사진찍기('동호'); 동호사진가져오기(); // 동호 사진
TypeScript
복사
사진 변수는 사진찍기 함수 안에서만 존재하는 지역 변수입니다. 보통은 함수가 끝나면 이 변수는 사라지죠.
그런데 사진가져오기 함수가 사진을 사용하고 있기 때문에, 자바스크립트는 이 사진 변수를 기억(=클로저) 해둡니다.
그래서 사진찍기가 이미 실행을 끝냈더라도, 사진가져오기는 여전히 동호의 사진을 기억하고 사용할 수 있습니다.
이처럼 데이터(사진)는 외부에서 직접 접근할 수 없고, 특정한 함수(사진사)를 통해서만 안전하게 접근할 수 있도록 하는 구조. 바로 이게 클로저가 존재하는 이유입니다.

클로저와 렉시컬 환경

자바스크립트는 함수가 호출될 때마다 실행 컨텍스트(Execution Context)를 생성하고, 그 안에 렉시컬 환경(Lexical Environment)이라는 공간을 만듭니다.
렉시컬 환경은 함수가 선언된 시점의 스코프를 기억하고 있으며, 함수 내부에서 선언된 변수들이 이 공간에 저장됩니다.
아래 사진관 예제를 통해 렉시컬 환경과 클로저 동작 과정을 자세히 알아봅시다.
function 사진찍기(고객이름) { const 사진 = `${고객이름} 사진`; function 사진가져오기() { console.log(사진); } return 사진가져오기; } let 동호사진가져오기 = 사진찍기('동호'); 동호사진가져오기(); // 동호 사진
TypeScript
복사

1. 전역 컨텍스트 생성

자바스크립트 코드는 실행하기 전, 전역 실행 컨텍스트(Global Execution Context)를 생성합니다.
전역 변수 및 함수 선언이 메모리에 등록됩니다. (호이스팅)
전역 렉시컬 환경 Record:
사진찍기: function 동호사진가져오기: <undefined>
Plain Text
복사
전역 렉시컬 환경 Outer:
null
Plain Text
복사

2. 전역 컨텍스트 실행

사진찍기(’동호’)가 실행됩니다.

3. 사진찍기 컨텍스트 생성

이 시점에 사진찍기 함수 내부의 렉시컬 환경이 만들어지고, 함수 인자인 고객이름과 내부 변수 사진, 그리고 내부 함수 사진가져오기가 등록됩니다.
중요한 점은 내부 함수 사진가져오기가 이 환경을 “기억”하고 있습니다. 이것이 바로 클로저가 생성되는 순간입니다.
사진찍기 렉시컬 환경 Record:
고객이름: '동호' 사진: '동호 사진' 사진가져오기: function
Plain Text
복사
사진찍기 렉시컬 환경 Outer:
전역 렉시컬 환경
Plain Text
복사

4. 사진찍기 컨텍스트 종료

사진찍기 함수가 반환하는 값은 내부 함수 사진가져오기입니다.
이 함수가 전역 변수 동호사진가져오기에 저장됩니다.
전역 렉시컬 환경 Record:
사진찍기: function 동호사진가져오기: function 사진가져오기()
Plain Text
복사
전역 렉시컬 환경 Outer:
null
Plain Text
복사
함수가 종료되면 일반적으로 함수 내부의 환경(지역 변수 등)은 사라집니다.
하지만, 반환된 내부 함수 사진가져오기가 여전히 사진찍기 렉시컬 환경을 참조하고 있기 때문에, 함수 종료 후에도 내부 변수에 접근 가능한 상태가 유지됩니다.

5. 내부 함수 실행

동호사진가져오기()가 호출되며, 내부 함수 사진가져오기 실행 컨텍스트가 생성됩니다.
사진가져오기 렉시컬 환경 Record:
// 지역 변수 없음
Plain Text
복사
사진가져오기 렉시컬 환경 Outer:
사진찍기 렉시컬 환경
Plain Text
복사
console.log(사진) 실행 시, 현재 실행 컨텍스트에 사진 변수가 없으니 상위 환경(사진찍기 렉시컬 환경)에서 사진을 찾아냅니다. (스코프 체인)
찾은 사진‘동호 사진’이 출력됩니다.

클로저와 메모리 관리

보통 함수가 끝나면 그 함수 안의 변수들은 메모리에서 사라져야 하는데, 클로저가 있기 때문에 사라지지 않고 남아 있다면 메모리에 계속 남아 있게 됩니다.
그런데, 이렇게 계속 기억하고 있으면 메모리가 부담되지 않을까요?
여기서 가비지 컬렉션(Garbage Collection, GC)이라는 친구가 등장합니다.
GC는 자바스크립트가 더 이상 사용하지 않는 메모리를 자동으로 찾아서 치워주는 청소부 같은 역할을 합니다.
예를 들어, 우리가 어떤 변수나 함수에 더 이상 손을 대지 않으면, GC는 이 메모리가 필요 없다고 판단하고 청소해 줍니다.
그런데 클로저가 변수를 계속 ‘참조’하고 있으면, GC는 그 변수를 “아직 필요한 거야!”라고 생각해서 청소를 하지 않습니다.
즉, 클로저가 존재하는 동안에는 클로저가 기억하고 있는 변수들이 계속 메모리에 남아 있게 되는 거죠.
사진관 예제로 다시 생각해보겠습니다.
let 동호사진가져오기 = 사진찍기('동호'); 동호사진가져오기(); // '동호 사진' 출력
TypeScript
복사
동호사진가져오기라는 함수가 사진 변수를 계속 잡고 있습니다.
그래서 동호사진가져오기를 호출할 때마다 사진에 접근할 수 있습니다.
동호사진가져오기 = null;
TypeScript
복사
만약 동호사진가져오기를 더 이상 사용하지 않고 변수도 삭제하면, GC가 이 변수의 환경을 메모리에서 청소해 줄 수 있습니다.

클로저 활용 사례

그렇다면, 클로저는 어디에 어떻게 활용될 수 있을까요?
대표적인 3가지 사례를 통해 클로저의 실전 활용법을 소개해보려 합니다.

1. 데이터 은닉과 캡슐화

자바스크립트에서는 private 키워드 없이도 데이터를 외부에서 숨길 수 있는 방법이 있습니다.
바로 클로저를 활용하는 것이죠.
function createCounter() { let count = 0; // 외부에서 접근 불가한 변수 return { increment() { count++; console.log(count); }, decrement() { count--; console.log(count); } }; } const counter = createCounter(); counter.increment(); // 1 counter.increment(); // 2 counter.decrement(); // 1 console.log(counter.count); // undefined!
TypeScript
복사
여기서 countcreateCounter 내부에만 존재하는 변수입니다.
외부에서는 접근할 수 없고, 오직 incrementdecrement를 통해서만 조작할 수 있죠.
이게 바로 캡슐화(encapsulation)의 효과입니다.

2. 상태 관리 및 유지

React처럼 컴포넌트 단위로 상태를 관리하는 라이브러리에서도, 그 핵심에는 클로저의 개념이 숨어 있습니다.
function createToggle() { let isOn = false; return function toggle() { isOn = !isOn; console.log(`지금 상태: ${isOn ? '켜짐' : '꺼짐'}`); }; } const toggleSwitch = createToggle(); toggleSwitch(); // 지금 상태: 켜짐 toggleSwitch(); // 지금 상태: 꺼짐
TypeScript
복사
매번 toggleSwitch를 호출할 때마다 isOn이라는 상태가 유지되고 변화합니다.
이처럼 어떤 함수가 실행될 때 내부 상태를 계속 기억하고 변화시키는 것은 클로저가 있기 때문에 가능한 일입니다.

3. 이벤트 핸들러와 콜백

클로저는 비동기 로직, 특히 이벤트 핸들러나 타이머 같은 콜백 함수와 함께 쓰일 때도 위력을 발휘합니다.
function setupButton(id) { let clickCount = 0; document.getElementById(id).addEventListener('click', () => { clickCount++; console.log(`버튼이 ${clickCount}번 클릭됨`); }); }
TypeScript
복사
clickCount는 외부에서는 접근할 수 없지만, 클릭 이벤트가 발생할 때마다 내부에서 값을 누적해서 기억합니다.
이게 가능한 이유는 콜백 함수가 실행될 때도 여전히 클로저가 살아 있기 때문입니다.

정리

1.
클로저는 함수가 선언될 당시의 렉시컬 환경을 기억하는 기능입니다.
2.
외부 변수에 직접 접근하지 않고 안전하게 값을 유지하거나 숨길 수 있습니다.
3.
실무에서는 데이터 은닉, 상태 유지, 콜백 함수 등에 자주 활용됩니다.

참고 자료