클로저(Closure)
자바스크립트에서 클로저를 이해하려면 먼저 스코프(Scope) 와 렉시컬 스코프(Lexical Scope) 개념에 대한 이해가 선행되어야 한다.
클로저와 관련된 개념들
렉시컬 스코프(Lexical Scope)
스코프(Scope) 란, 선언된 변수에 접근할 수 있는 범위를 의미한다.
자바스크립트는 렉시컬 스코프를 사용한다. 이는 함수가 선언된 시점을 기준으로 스코프가 결정된다는 뜻이다.
좀 더 쉽게 말해보면, 어떤 함수를 선언하면 함수 밖에서 함수에 선언된 변수에 접근할 수 있다는 것이다. 이렇게 **외부 함수의 스코프(환경)**에 접근할 수 있는 함수가 만들어지는 현상을 클로저(Closure) 라고 한다.
스코프 체이닝(Scope Chaining)
자바스크립트 엔진은 함수를 실행할 때, 먼저 현재 실행 컨텍스트(Execution Context) 내에서 필요한 변수를 찾고, 없으면 바깥 스코프를 따라 차례로 올라가며 탐색한다. 이를 스코프 체이닝(Scope Chaining) 이라고 한다.
클로저(Closure)
function Dog() {
let name = "멍멍이";
return {
callName() {
console.log(name);
},
rename(newName) {
name = newName;
},
};
}
const dog = Dog();
dog.callName(); // 멍멍이
dog.rename("복슬이");
dog.callName(); // 복슬이
위 예제에서 callName
함수는 Dog
함수 내부에서 정의되었고, 함수 바깥에서 선언된 name
이라는 지역 변수에 접근하고 있다.
중요한 점은 Dog
함수가 실행된 이후에도 callName
함수는 여전히 name
변수를 참조할 수 있다는 것이다. 이는 callName
이 생성될 때의 렉시컬 환경(스코프) 을 기억하고 있기 때문이다.
이처럼 함수가 외부 함수의 지역 변수에 접근할 수 있는 현상이 바로 클로저(Closure) 이다.
이 구조를 활용하면 외부에서는 name
변수에 직접 접근하지 못하고, callName
과 같은 내부 함수로만 접근할 수 있다. 즉, 클로저를 통해 데이터 은닉과 캡슐화 를 구현할 수 있다.
상태를 기억하는 함수 만들기
아래는 클로저를 통해 상태를 유지하는 함수의 예시이다.
function createCounter() {
let count = 0;
return function go() {
console.log(count++);
};
}
const counter1 = createCounter();
counter1(); // 0
counter1(); // 1
const counter2 = createCounter();
counter2(); // 0
이 예제에서는 counter1, counter2가 각각의 상태를 기억하고 잇다.
createCounter
를 호출할 때마다 새로운 스코프와 count
변수가 생성된다.
go
함수는 이 변수를 계속 참조하므로, 독립적인 상태를 유지할 수 있다.
클로저가 불러오는 불확실성
클로저는 매우 유용하지만, 예측하기 어려운 동작을 유발하는 원인이 된다.
특히, 어떤 변수가 어떤 클로저에 포함되어 있는지 파악하기 어렵다면 코드의 의도를 해석하기 어려워진다.
함수형 프로그래밍에서는 순수 함수(Pure Function) 의 사용을 강조한다.
클로저는 외부 상태를 캡처하므로, 해당 함수는 더 이상 순수하지 않게 되고, 결과적으로 불변성과 예측 가능성 이 약해질 수 있다.
메모리 누수와 클로저
자바스크립트는 가비지 컬렉터를 통해 사용되지 않는 메모리를 자동으로 회수한다.
하지만 클로저로 인해 참조가 끊기지 않은 변수는 계속 메모리에 남아 있게 된다.
예를 들어, 이벤트 리스너나 타이머 내부에서 클로저를 사용할 경우, 해당 함수가 제거되지 않으면 참조된 변수도 계속 메모리에 유지된다.
메모리 누수 방지하기
메모리 누수를 방지하기 위해서는, 사용이 끝난 함수에 null
을 할당하거나, 이벤트 리스너를 명시적으로 제거하는 등의 처리가 필요하다.
클로저의 활용 사례
1. 정보 은닉 (Information Hiding)
자바스크립트는 기본적으로 private
키워드가 없었지만, ES2022부터 #
접두사를 사용한 private 필드가 도입되었다. 하지만 클로저를 사용하면 이전 버전에서도 마치 프라이빗 변수처럼 외부 접근을 차단할 수 있다.
function createSecureCounter() {
let count = 0;
return {
increment() {
count++;
return count;
},
reset() {
count = 0;
},
};
}
const counter = createSecureCounter();
console.log(counter.increment()); // 1
console.log(counter.count); // undefined (직접 접근 불가)
2. 콜백이나 비동기 처리에서의 변수 캡처
클로저를 이용하면 비동기 함수에서 생성된 값을 고정시킬 수 있다. 아래 예시처럼 루프 내에서 생성된 값을 고정시켜 사용할 수 있다.
for (let i = 0; i < 3; i++) {
(function (j) {
setTimeout(() => console.log(j), 1000);
})(i);
}
// 0, 1, 2 출력
실전에서 클로저는 얼마나 자주 쓰일까?
React에서는 useState
, useEffect
등의 훅이 내부적으로 클로저 개념을 사용하고 있다.
직접 클로저를 만들어 쓰지 않더라도, 클로저의 동작 원리를 이해하고 있으면 React의 동작 방식에 대해 훨씬 깊은 이해가 가능하다.
마무리
클로저는 자바스크립트의 핵심 개념 중 하나이며, 자바스크립트를 잘 쓰기 위해서는 반드시 이해해야 하는 개념이다. 다만, 지나치게 복잡한 클로저 구조는 예측을 어렵게 만들고, 메모리 누수를 유발할 수 있으므로 필요한 곳에만 신중하게 사용하는 것이 바람직할 것 같다.