요즘 꾸준히 읽고 있는 Medium 글에서 "클로저를 모르고 자바스크립트를 한다는 건, 영어 문법에 대한 이해도 없이 영어 말하기를 하는 것과 같다."는 문장을 읽고 클로저에 대해 TIL을 써보기로 했습니다.
클로저는 자바스크립트뿐만 아니라 여러 함수형 프로그래밍 언어도 가지고 있는 개념이다. 클로저에 대한 일반적인 정의를 나열해보자면,
💡 A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.
💡 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수 - 존 레식
💡 자신이 생성될 때의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행될 때 사용할 변수들만을 기억하여 유지시키는 함수 - 유인동
💡 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저라고 부른다. - 이웅모
💡 클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상. - 정재남
즉 함수 안에서 함수를 선언하고 사용하는 상황에서 클로저란 함수와 함수의 lexical 스코프를 포함하는 걸 뜻한다. 생성될 당시의 환경을 기억하는 함수라고 생각하자.
여기서 렉시컬 스코프란?
자바스크립트는 렉시컬 스코프를 따르는 프로그래밍 언어이다. 자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다. 이를 렉시컬 스코프(정적 스코프)라 한다. 다른 말로 실행 컨텍스트의 렉시컬 환경이라 한다.
외부 함수 안에 내부 함수가 있는 상황에서, outer 함수의 실행이 종료되면 inner 함수를 반환하면서 outer 함수의 생명 주기가 종료된다. 즉, outer 함수의 실행 컨텍스트가 실행 컨텍스트 스택에서 제거된다. 이때 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다. outer 함수의 렉시컬 환경은 inner 함수에 의해 참조되고 있고 inner 함수는 전역 변수 innerFunc에 의해 참조되고 있으므로 가비지 컬렉션의 대상이 되지 않기 때문이다. 가비지 컬렉터는 누군가가 참조하고 있는 메모리 공간을 함부로 해제하지 않는다.
정재남, <코어자바스크립트>
즉 클로저를 사용하면 외부함수의 실행 컨텍스트가 스택에서 제거되어도 내부함수에 있는 변수에 접근할 수 있다.
Closures are frequently used in JavaScript for object data privacy, in event handlers and callback functions, and in partial applications, currying, and other functional programming patterns.
클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다. 즉 상태가 의도치 않게 변경되지 않도록 상태를 안전하게 은닉하고, 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용한다.
자바스크립트는 클래스가 없고 전역변수를 기반으로 하기 때문에 변수가 외부에 노출된다. (클래스에는 public, private, protected 같은 접근 제한자를 선언해서 프로퍼티와 메서드의 공개 범위를 한정할 수 있는데 자바스크립트에는 이러한 제한자가 없고 기본적으로 public 하다.) 따라서 자바스크립트에서는 이 클로저를 이용해서 private한 변수를 만들 수 있다.
const num = 1;
const foo = () => {
const num = 3;
const bar = () => {
console.log(num);
}
return bar;
}
const baz = foo();
baz();
여기서 클로저는 bar() 함수이다. 이 함수의 렉시컬 스코프는 자신이 생성된 foo()의 스코프라고 할 수 있다. baz라는 변수에 담긴 foo()의 스코프가 클로저인 bar의 렉시컬 스코프가 되기 때문이다. 따라서 baz()가 전역적으로 실행이 되더라도 const num = 1
이라는 전역 변수를 가져오는 것이 아니라, 자신의 렉시컬 스코프인 foo() 함수 내의 const num = 3
을 가져오게 된다.
다음의 함수를 보자.
const increase = () => {
let num = 0;
return ++num;
}
console.log(increase());
console.log(increase());
console.log(increase());
console.log(increase());
이 결과값은 어떻게 될까? 답은 1, 1, 1, 1이다. increase()라는 함수가 실행이 종료되면 가바지 컬렉터에서는 참조가 끝난 데이터를 없애버리기 때문에 항상 increase() 함수 안에 있는 num은 0으로 초기화된다.
그렇다면 다음의 함수는?
const foo = () => {
let num = 0;
return () => {
return ++num;
}
}
const increase = foo();
console.log(increase());
console.log(increase());
console.log(increase());
console.log(increase());
이 함수의 결과는 1, 2, 3, 4가 된다.
increase라는 변수에 foo()라는 함수를 담고, 그 안에는 클로저를 넣었다. 이 때의 클로저는 num
이라는 변수를 가비지 컬렉터가 수거하지 않도록 참조하며 변수를 안전하게 유지해준다.
이러한 경우에 메모리를 계속 차지하고 있으므로 더이상 클로저를 사용하지 않을 때에는 null을 이용해서 메모리를 초기화해 주어야한다.
참고 자료
https://meetup.toast.com/posts/86
코어자바스크립트
자바스크립트 딥다이브
https://medium.com/dailyjs/i-never-understood-javascript-closures-9663703368e8
클로저... 참 어렵죠
단어가 생소하기도 하고 개념도 어렵고 설명도 어렵고...
저도 처음에 참 이해하기가 어려웠는데...
제가 이해한것을 말해줄게요~~
위에서 예시를 든 코드를 다시보면
const num = 1;
const foo = () => {
const num = 3;
const bar = () => {
console.log(num);
}
return bar;
}
const baz = foo();
baz();
foo()로 foo 함수를 실행하게 되면 foo 함수 내부를 쭉 읽어내려가다가 마지막에 bar 함수를 리턴하고 종료가 되죠
이때 foo 함수는 함수의 주기가 종료되었기 때문에 지역변수 const num = 3도 함께 소멸되요
그런데 이렇게 리턴되는 bar 함수를 baz라는 변수에 저장을 하고
baz()로 bar 함수 실행하게 되면 bar 함수 내부에서는 아까 소멸되었어야 정상인 foo 함수의 const num = 3을 참조할 수가 있어요
bar 함수는 마치 아래와 같이 동작을 해요
const num = 3 ;
const bar = () => {
console.log(num)
}
아래 예시를 든 코드에서도 마찬가지로
const foo = () => {
let num = 0;
return () => {
return ++num;
}
}
const increase = foo();
increase();
increase();
increase();
increase 라는 변수에는 foo 함수를 실행하고 난 익명의 화살표 함수가 리턴이 되겠죠?
그럼 또 이렇게 되는거에요
let num = 0;
function () => {
return ++num;
}
이 상태에서 increase()를 계속하게 되면
당연히 우리가 아는대로 1, 2, 3 이렇게 리턴이 되는거죠
그런데 이때 주의해야 할 점은 반드시 리턴되는 함수를 변수에다가 한번 담아준 후에 그 변수를 통해서 실행을 해주어야 한다는 점이에요
그냥 바로 이렇게 foo()() 실행을 해버리면 참조관계가 끊어져버려서 얘처럼 동작을 해요
const increase = () => {
let num = 0;
return ++num;
}
increase();
increase();
increase();
그래서 마무리를 하자면 이러한 현상은 클로저 ~ 변수에 담기는 함수는 클로저함수 ~ 중첩함수를 사용할 때 외부 함수의 라이프사이클이 끝났음에도 불구하고 내부함수가 외부함수의 값을 계속해서 참조를 할 수 있는 것을 말한다~~