

영화 코코(coco)를 아시나요?
영화 속 수많은 장면들 중에 사진과 같은 장면이 있다. 클로저 에 대해 알아보기 전에 이 사진을 기억하고서 시작해보면 좋을 것 같아 사진을 넣어보았다.
사진을 기억하며 클로저에 대해서 알아보자! (remember me 🎶)

클로저에 대해 이해하기위해 이전에 다뤘던 내용들은 다시 살펴보자.
1️⃣ 13장.스코프에서 렉시컬 스코프에 대해서 다음 내용을 살펴보았다.
즉, 함수의 상위 스코프는 함수를 정의한 위치에 의해 정적으로 결정이 되고난 이후에는 변하지 않는다.
2️⃣ 23장.실행컨텍스트에서 스코프 체인에 대해서 다음 내용을 살펴보았다.
위 두 가지 내용을 종합하여 렉시컬 스코프에 대한 개념을 다음과 같이 잡아두고 클로저에 대해서 알아보자.
상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경에 의해 결정된다.
함수가 정의되는 환경과 호출되는 환경은 다를 수 있다.
그럼 호출되는 환경이 정의된 환경과 다르더라도 정의된 환경에 대해 기억을 해야하지 않을까?
⇒ 정의된 환경에 대해 기억하는 공간이 바로 함수 자신의 내부 슬롯 [[Environment]] 이다.
더 자세히 살펴보자면, 함수 정의가 평가되어 함수 객체를 생성할 때 자신이 정의 된 환경에 의해 결정된 상위 스코프의 참조를 함수 객체 자신의 내부 슬롯 [[Environment]] 에 저장한다.
이 때, 내부슬롯에 저장되는 상위 스코프의 참조는 바로 현재 실행 중인 실행 컨텍스트의 렉시컬 환경 인 셈이다.
함수 정의가 평가되어 함수 객체를 생성하는 시점은 상위 함수가 평가 또는 실행되고 있는 시점이고, 이 때 실행 중인 실행 컨텍스트는 상위 함수의 실행 컨텍스트이기 때문이다.
정리해보자면,
함수 객체의 내부 슬롯에 저장된 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조는 상위 스코프이며, 자신이 호출되었을 때 생성될 '외부 렉시컬 환경에 대한 참조'에 저장될 참조값이기도 하다.
함수 객체는 내부 슬롯에 저장한 상위 스코프를 자신이 존재하는 한 기억하는 것이다. (like coco)
관련하여 예시로 한번 더 살펴보자.
const x = 'squirrel'
function moo() {
const x = 'minji'
hoo() // hoo() 호출위치 → 정의된 위치의 렉시컬 환경에서의 기억을 소환(squirrel)
}
function hoo() {
// hoo() 정의위치(전역) → 전역 렉시컬 환경 기억(squirrel)
console.log(x)
}
moo() // squirrel
hoo() // squirrel
const x = 'squirrel'
function outer() {
const x = 'minji'
const inner = function () {console.log(x)}
return inner
}
// 위 함수가 실행이 되고 outer() 함수는 제 역할을 다 했기 때문에
// 스택에서 pop 되어 사라짐
const innerFunc = outer() // outer() 함수 호출!
innerFunc() // 'minji' ⁉️
outer() 함수의 지역변수 값인 minji 이 예시의 결과값으로 나왔다.
outer() 함수는 호출되고나서 inner함수를 반환하며 제 역할을 다 했기때문에 스택에서 제거가 되었다.
스택에서 제거되면서 가지고 있던 지역변수도 함께 제거가 되었을텐데 어떻게 minji이라는 결과가 나온 것일까?
이 말에 대해서 자세하게 살펴보자.
자바스크립트의 모든 함수는 자신의 상위 스코프를 기억하고, 기억한 상위 스코프는 함수의 호출위치에 상관없이 계속 기억하고있다.
위 예시에서 inner 함수는 자신이 평가될 때 상위 스코프인 outer 를 내부 슬롯에 저장하였다.
즉, outer 함수의 렉시컬 환경이 inner 함수의 내부 슬롯에 저장되어 기억되고 있는 것이다.
그렇기 떄문에 outer 함수의 실행 컨텍스트가 제거되었어도 렉시컬 환경까지 소멸되지는 않은 것이다.
inner 함수에서 outer 함수의 렉시컬 환경을 기억(참조)하고 있기때문에 가비지 컬렉터도 함부로 작업을 시작하지 못한다.
✋🏻 잠깐!
자바스크립트의 모든 함수는 자신의 상위 스코프를 기억한다고 했는데, 그렇다면 중첩 함수는 모두 클로저인가?
아래 예시들을 통해 성립조건을 알아보자.
function foo() {
const x = 1
const y = 2
function bar() {
const z = 3
console.log(z)
}
return bar
}
const bar = foo()
bar()
위 예시에서 bar 함수는 중첩함수니까 클로저라고 생각할 수 있다.
하지만 여기서 bar 는 클로저가 아니다.
왜? bar 는 외부 함수인 foo 함수의 중첩 함수이긴 하지만, foo 함수의 어떤 식별자도 참조하고 있지 않기 때문이다.
성립조건 1. 상위 스코프의 식별자를 참조하라
자, 이제 다른 예시를 살펴보자.
function foo() {
const x = 1
function bar() {
console.log(x)
}
bar()
}
foo()
이 예시에서의 bar 함수는 중첩 함수에다가 foo 함수의 식별자도 참조하고있다. 그렇다면 bar 는 클로저!?!?
안타깝게도 이번에도 땡! 이다 🙂↔️ (대체 왜! 뭐때문에!)
이유는 중첩 함수 bar가 외부 함수 foo 의 외부로 반환되지 않았기 때문이다.
즉, 외부 함수보다 중첩 함수가 오래 살아남아야하는데 예시에서는 중첩 함수의 생명주기가 외부 함수보다 짧다. (bar 는 아주 찰나의 클로저였달까..)
성립조건 2. 외부 함수보다 오래 살아남아라
위 성립조건 두 가지를 충족하는 bar 함수가 클로저가 될 수 있는 예시는 다음과 같다.
function foo() {
const x = 1
const y = 2
function bar() {
console.log(x)
}
return bar
}
const bar = foo()
bar()
✋🏻 잠깐! 위 예시에서 중첩 함수 bar 는 foo 함수의 식별자 중 x 만 참조를 하고 있다.
그렇다면 참조되지않는 y 는 어떻게 되는걸까?
소중한 메모리 공간을 생각하여 클로저가 참조하지 않는 식별자는 기억되지 못하고 GC(Garbage Collector)에게 수거된다.
이처럼 클로저에 의해 참조되는 상위 스코프의 변수는 자유 변수(free variable) 이라고 부른다.
여기서 클로저라는 말의 의미와 연결될 수 있는데,
클로저란 "함수가 자유 변수에 묶여있다. (닫혀있다)" 라는 의미이다
클로저는 자바스크립트의 강력한 기능 중 하나로 적극적으로 활용하면 유용하게 사용될 수 있다.
유용하게 사용되는 상황들 중 대표적인 상황을 살펴보자.
즉, 상태가 의도치않게 변경되지 않도록 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용되는 것이다.
아래 예시와 함께 살펴보자
// 문제상황 👿
let num = 0
const increase = function() {
return ++num
}
console.log(increase()) // 1
console.log(increase()) // 2
console.log(increase()) // 3
위 예시에서 함수 내에서 사용되는 변수는 전역 변수를통해 관리되고 있다.
이렇게되면 언제든지, 누구나 접근을 할 수 있기 때문에 의도치 않게 값이 변경될 수 있고 그렇게 되면 오류발생으로 이어진다.
카운트 상태를 나타내는 전역 변수인 num 을 다음과같이 안전하게 은닉하는 것이 필요하다.
const increase = function() {
let num = 0 // 은닉 완료 😎
return ++num
}
console.log(increase()) // 1
console.log(increase()) // 1 ⁉️
console.log(increase()) // 1 ⁉️
이제 num 변수는 increase 함수의 지역 변수로 변경되어 오염될 일은 사라졌다.
그런데 또 다른 문제가 있다.
바로 increase 함수가 호출될 때마다 지역 변수인 num 이 초기화 되어 출력 결과가 언제나 동일하다는 것이다.
이전 상태를 유지해야 num이 하나씩 증가하는 함수의 목적을 실현시킬 수 있다.
이 때 클로저를 사용해보면 어떨까?
const increase = (function() {
let num = 0
// 클로저!
return function() {
return ++num
}
}())
console.log(increase()) // 1
console.log(increase()) // 2
console.log(increase()) // 3
즉시 실행함수가 호출된 이후 클로저 함수를 반환하고 사라진다.
하지만 클로저 함수가 자유변수 num 을 기억하고 있기 때문에 초기화 되지않고 이전 상태를 기억하고 num이 하나씩 증가는 결과를 가져온다.
| 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 메서드를 하나로 묶는 것을 말한다.
| 위에서 말한 캡슐화가 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용되는 것을 말한다.
자바스크립트는 다른 객체지향 프로그래밍 언어와 다르게 public, private, protected 와 같은 공개범위를 한정할 수 있는 접근 제한자를 제공하지 않는다.
그렇기 때문에 자바스크립트 객체의 모든 프로퍼티 와 메서드는 기본적으로 외부에 공개되는 public 으로 선언되어 있다.
인스턴스 메서드와 프로토타입을 활용하여 private 흉내를 낼 수는 있지만 완전하다고 할 수는 없다.
이와 관련한 자세한 내용은 다음장인 25장.클래스에서 살펴보자.
다음은 클로저를 사용할 때 자주 발생할 수 있는 실수를 보여주는 예시이다.
var funcs =[]
for (var i = 0; i < 3; i++) {
funcs[i] = function() {return i}
}
for (var j = 0; j < funcs.length; j++) {
console.log(funcs[j]())
}
위 예시에서 기대하는 결과값은 0, 1, 2 이다.
안타깝게도 실제 결과값은 3 이 출력된다.
이유는 for문에서 변수 선언시 var 키워드로 선언하였기 때문이다.
13장.스코프에서도 살펴봤듯 var키워드는 블록 레벨 스코프가 아니라 함수 레벨 스코프를 갖기때문에 전역 변수이다.
그렇기 떄문에 최종적으로 funcs[j] 함수를 호출하면 3이라는 결과값이 나온다.
위 예시를 기대하는 결과값으로 도출하기 위해서는 어떻게 수정하면 좋을까?
책에 나와있는 클로저를 사용한 방법은 우리가 자주 보던 형태는 아니다. (그리고 복잡해보인다)
우리에게 익숙한, 자주 보던 형태로 수정해보자!
const funcs = []
for (let i = 0; i < 3; i++) {
funcs[i] = function() {return i}
}
for (let i = 0; i < funcs.length; i++) {
console.log(funcs[i]())
}
const 와 let 을 보니 마음이 편-안해진다. 😊
이처럼 let 키워드로 선언한 변수를 사용하면 반복 실행될 때마다 for문 코드 브록의 새로운 렉시컬 환경이 생성되며 식별자의 값을 유지한다. (like 스냅샷)
단, 함수가 반복문의 코드 블록 내부에서 정의되어야 적용이 되는 내용이다.
반복문의 코드 블록 내부에 함수 정의가 없다면 생성된 렉시컬 환경을 참조하는 곳이 없기때문에 가비지 컬렉션의 타겟이 된다. 👀
| 외부 함수(outer)보다 중첩 함수(inner)가 더 오래 유지되는 경우, 중첩 함수가 제 역할을 다 끝낸 외부 함수의 변수를 참조하는 것
1️⃣ 상위 스코프의 식별자를 참조하라
2️⃣ 외부 함수보다 오래 살아남아라
| 상태가 의도치않게 변경되지 않도록 은닉하고 특정 함수에게만 상태 변경을 허용하고자할 때 활용하자