클로저는 갑자기 등장한 개념이 아니다.
보통 하나의 의문에서 시작된다.
다음 코드를 보자.
function outer() {
const message = "hello";
function inner() {
console.log(message);
}
return inner;
}
const fn = outer();
fn();
코드의 실행 흐름을 보면 다음과 같다.
outer 실행
→ inner 반환
→ outer 종료
→ fn 실행
여기서 중요한 질문이 하나 생긴다.
outer() 함수는 이미 실행이 끝났다.
그런데 inner() 함수는 message 변수에 계속 접근한다.
즉 이런 의문이 생긴다.
이미 종료된 함수의 변수에 어떻게 접근할 수 있을까?
보통 함수가 끝나면
그 함수 내부의 변수도 사라지는 것이 정상이다.
하지만 위 코드에서는
message가 계속 유지된다.
이 현상을 설명하기 위해 등장하는 개념이 클로저(Closure)다.
클로저를 이해하려면 먼저 렉시컬 스코프를 떠올려야 한다.
JavaScript에서는
함수의 스코프가 호출 위치가 아니라 선언 위치에 의해 결정된다.
예제를 보자.
function outer() {
const a = 10;
function inner() {
console.log(a);
}
inner();
}
outer();
여기서 inner() 함수는
outer() 함수 안에서 선언되어 있다.
그래서 inner()는
자신이 선언된 스코프에 있는 변수 a에 접근할 수 있다.
이 규칙이 바로 이전 글에서 다뤘던 렉시컬 스코프다.
구조를 보면 이렇게 된다.
global scope
⎿ outer
⎿ inner
하위 스코프는
항상 상위 스코프의 변수에 접근할 수 있다.
여기까지는 사실 클로저가 아니다.
단순히 스코프 규칙일 뿐이며 이전 글의 복습에 가깝다.
클로저는 조금 다른 상황에서 등장한다.
외부 함수는 종료됐지만 내부 함수가 외부 변수를 계속 사용하는 상황이다.
즉 핵심 상황은 다음과 같다.
outer 함수는 이미 끝났지만
inner 함수는 여전히 outer의 변수를 사용한다.
이때 JavaScript는
외부 스코프를 그대로 유지한다.
이 상태를 클로저라고 한다.
보통 클로저는 다음처럼 설명된다.
클로저는 함수와 그 함수가 선언될 당시의 렉시컬 스코프가 결합된 구조다.
즉 클로저는 단순히 함수 하나를 의미하는 것이 아니다.
구조적으로 보면 다음과 같다.
함수
+
함수가 선언될 당시의 스코프
=
클로저
이 말은 JavaScript에서 함수는 실행될 때만 동작하는 것이 아니라
자신이 선언된 환경을 기억한다는 의미다.
그래서 내부 함수는
자신이 선언된 위치의 스코프에 계속 접근할 수 있다.
이제 처음 던졌던 질문으로 다시 돌아가 보자.
function outer() {
const message = "hello";
function inner() {
console.log(message);
}
return inner;
}
const fn = outer();
fn();
코드의 흐름은 다음과 같다.
outer 실행
→ inner 반환
→ outer 종료
→ fn 실행
일반적으로 함수가 끝나면
그 함수 내부 변수는 메모리에서 제거된다.
하지만 위 코드에서는
message가 계속 유지된다.
그 이유는 inner 함수가
message 변수에 계속 접근해야 하기 때문이다.
그래서 JavaScript 엔진은
outer의 렉시컬 환경을 바로 제거하지 않는다.
대신 inner 함수와 함께 유지한다.
구조적으로 보면 이렇게 된다.
inner 함수
+
outer의 스코프(message)
이처럼 함수와 외부 스코프가 함께 유지되는 상태를
클로저(Closure)라고 한다.
클로저는 단순히 개념으로만 존재하는 것이 아니라
실제 코드에서 특정 패턴을 만들기 위해 자주 사용된다.
대표적인 활용 방식은 크게 두 가지다.
클로저를 사용하면 함수 내부 상태를 유지할 수 있다.
function counter() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
const increase = counter();
increase(); // 1
increase(); // 2
increase(); // 3
이 코드에서 중요한 점은 다음과 같다.
count 변수는 counter 함수 내부에 존재한다count에 직접 접근할 수 없다count에 계속 접근할 수 있다즉 클로저를 통해 함수 내부 상태가 계속 유지된다.
클로저는 외부에서 직접 접근할 수 없는 데이터를 만드는 데도 사용된다.
function createUser(name) {
let _name = name;
return {
getName() {
return _name;
},
};
}
const user = createUser("Alice");
console.log(user.getName()); // Alice
여기서 _name 변수는
이처럼 클로저는 데이터를 보호하는 구조를 만들 때도 활용된다.
함수 내부에서 선언된 변수는 함수 내부에서만 사용할 수 있다는 것은 알고 있었지만, 함수 실행이 끝나면 메모리에서도 사라진다는 점은 이번에 처음 알게 되었습니다. 어떻게 보면 당연한 개념일 수도 있지만, 덕분에 새롭게 이해하고 갑니다! 👍