서론
자바스크립트 단골 면접 질문이라는 클로저에 대해 다뤄보려고 한다.
사실 2주전에 클로저에 대해 알아보다가 도망친 기억이 있다.
하지만 실행 컨텍스트에 대해 깊게 다뤄보았기 때문에 이번 기회에 클로저를 졸업할까 한다.
알고 가면 좋은 배경 지식
내가 생각하기에 클로저를 배우기 전에 알고 넘어가면 좋을 것 같은 부분을 가져와봤다.
자바스크립트는 정적 스코프(렉시컬 스코프)이다.
즉, 함수의 스코프는 어디서 정의됐느냐
에 따라 적용된다.
1
2
3
4
5
6
7
8
9
const x = 1;
function outer() {
const x = 10;
function inner() {
console.log(x); // 10
}
inner();
}
outer();
위의 inner 함수가 x를 가져올 수 있었던 이유는 outer 함수 내에서 정의되었기 때문이다.
즉, inner 함수는 outer 함수의 스코프를 가진다.
또 다른 예를 확인해보자.
1
2
3
4
5
6
7
8
9
const x = 1;
function outer() {
const x = 10;
inner();
}
function inner() {
console.log(x); // 1
}
outer();
위의 코드는 뭔가 될 것 같다..? 아니다 어림도 없다 1을 출력한다.
inner함수가 outer 안에서 호출되었지만 outer의 스코프를 갖지 않기 때문에 x의 값을 가져올 수 없다.
위에서 이야기한 것 처럼 함수의 스코프는 어디서 호출됐냐가 아니라 어디서 정의됐냐에 따라 적용된다.
함수는 선언되거나 표현식이 실행되었을때 함수 객체를 생성하는데
그 과정에서 함수 객체의 내부 슬롯 [[Enviroment]]
안에 상위 스코프에 대한 참조를 저장한다.
이 후 실행 컨텍스트가 생성되는 과정에서 외부 렉시컬 환경에 대한 참조(Outer)에 내부 슬롯 [[Enviroment]]
에 저장된 값이 할당된다.
클로저란?
mdn에서는 ‘함수와 그 함수가 선언된 렉시컬 환경과의 조합’ 이라고 정의한다. 머라는거야
위의 정의로는 내가 명확하게 대답할 수 없을 것 같았고, 고민 끝에 대답을 도출해냈다.
상위 스코프의 식별자를 참조
하고 있고, 본인의 외부 함수보다 오래 유지
되어 있는 경우를 클로저라고 한다.
쉽게 말하자면 내부 함수가 생성될 당시의 외부 함수의 변수를 기억하고 있는 녀석이다.
TMI 하자면 클로저가 참조하고 있는 상위 스코프의 식별자를 자유변수라고 한다.
이해를 돕고자 아래 코드와 그림을 보면서 이해해보자.
1
2
3
4
5
6
7
8
9
10
const x = 1;
function outer() {
const x = 10;
const inner = function () {
console.log(x);
};
return inner;
}
const closure = outer(); // 2~3번 과정
closure(); // 10 // 4번 과정
이 그림은 실행 컨텍스트가 어떻게 구성되어있는지에 대한 이해가 필요하다..! 이 글을 읽고 오자
그래도 짧게 설명해보자면 실행컨텍스트 안에는 렉시컬 환경(스코프 내 변수 값 저장)
과 Outer(상위 스코프의 참조)
로 구성되어 있다.
이어서 계속 진행해보자면 위의 코드가 콜스택의 쌓이는 과정을 보면 이렇게 진행된다.
- 전역 실행 컨텍스트가 생성되고 콜스택에 쌓인다.
- 전역 실행 컨텍스트가 실행되면서 outer 함수 실행 컨텍스트가 생성되고 콜스택에 쌓인다.
- outer 함수 실행 컨텍스트가 실행되고, 내부에는 x와 inner에 대한 값을 렉시컬 환경에 저장한 후 콜스택에서 빠져 나간다.
- 전역 실행 컨텍스트가 이어서 실행되고, inner 함수 실행 컨텍스트가 실행된다.
중요하게 봐야할 점은 outer 함수 실행 컨텍스트가 빠져 나간 후 inner 함수 실행 컨텍스트가 콜스택에 올라간다는 점이고, 그럼에도 inner 함수는 어떻게 outer의 변수를 참조가 가능하냐는 것이다.
일반적으로 실행 컨텍스트가 콜스택에서 빠져나갈때 내부 구성요소들도 같이 빠져 나간다.
하지만 3번에서 inner가 저장될때 [[Enviroment]]
에 outer 함수의 렉시컬 환경을 참조한다.
outer 실행 컨텍스트가 콜스택에서 빠져 나갔음에도 렉시컬 환경은 참조 되고 있기 때문에
가비지 컬렉터의 대상이 되지 않아 렉시컬 환경만은 살아남는다..! 중꺽마
클로저의 활용
사실 클로저를 이해하는데는 생각보다 그렇게 오래 걸리지 않았다.
하지만 클로저 어떻게 활용해야할까라는 물음에는 오랜 고민이 되었고,
교재 기준으로 유용하다는 생각이 들었던 부분들을 정리한다.
클로저를 활용하면 상태를 안전하게 은닉 하고, 특정 함수에게만 변화를 주게 할 수 있다.
즉, 사이드 이펙트 관리에 용이하다.
1. 생성자 함수 생성 시 활용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 클로저를 활용한 생성자 함수
const Counter = (function () {
let count = 0;
function Counter() {
// this.count = 0; // this를 사용하면 인스턴스마다 count 프로퍼티가 생성된다.(public)
}
Counter.prototype.increase = function () {
return ++count;
};
Counter.prototype.getCount = function () {
return count;
};
Counter.prototype.decrease = function () {
return --count;
};
return Counter;
})();
const counter = new Counter();
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.getCount()); // 2
만약 생성자 내부에 count를 선언했다면 생성될 인스턴스가 모두 같은 count를 사용하게된다.
변수 값을 생성된 인스턴스가 모두 접근 할 수 있기 때문에 로직 상 오류가 발생 할 수 있다.
생성자의 활용 예처럼 외부 상태 변경이나 가변 데이터를 피하고 불변성을 지향하는 함수형 프로그래밍에서 클로저는 적극적으로 활용된다.
2. 함수형 프로그래밍으로 활용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 클로저를 활용한 함수형 프로그래밍
function makeCounter(aux) {
let count = 0;
return function () {
count = aux(count);
return count;
};
}
function increase(n) {
return ++n;
}
function decrease(n) {
return --n;
}
const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2
위의 경우처럼 증가, 감소 함수 같이 함수를 따로 만들어서 적용하고 싶은 기능을 쉽게 추가할 수 있다.
주저리
요새 배우는게 너무 즐겁다. 하지만 책임도 따르는 법인 것 같다.
예를 들어 클로저를 배웠지만 클로저에서 파생된 숙제들이 쌓이고 있다. 커링 등등..
모든 것을 딥다이브 하고 싶지만 현재 상황이 그렇게 여유있지가 않다.
빨리 취업하는게 무조건 내 성장에 도움이 될까라는 의심도 들기에 고민이 많다.
Reference
- 모던 자바스트립트 Deep Dive
- 라매개발자님 유튜브 - 실사용 예시
- 우테코 엘라님
- mdn - closure