Intro
Closure에 대해 개념은 알고 있었지만, 이것을 왜 쓰는지, 언제 쓰는지에 대해서는 확실하게 이해하지 못하고 있었습니다.
그래서 이번 포스팅에서, Closure의 활용 예제들을 통해 Closure에 대해 더 깊게 이해해보도록 하겠습니다.
Closure란?
Closure란 무엇일까요?
Closure는 한 마디로 “lexical scope와 함수의 조합” 이라고 할 수 있습니다.
여기서 lexical scope란 그 함수가 선언 된 환경이라고 말할 수 있는데요, lexical scope에 대한 이해가 부족하신 분은 scope를 정리한 포스팅을 보고 오시는것을 추천드립니다.
저도 처음에는 이게 도대체 무슨 소리인지 와닿지 않았지만, 예제를 보시면 금방 이해하실 수 있습니다.
아래는 closure를 사용한 counter 예제입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const counter = () => {
let count = 0;
return () => {
console.log(count++);
};
};
let count = 10;
const printCount = counter();
printCount(); // 0
printCount(); // 1
counter
함수가 실행되면서 내부 함수를 리턴하고 printCount
변수에 저장합니다.
printCount
함수가 실행되면 실행되는 위치의 환경이 아닌, 내부 함수가 선언 된 환경, 즉 lexical scope의 count
변수를 참조하는 것을 볼 수 있습니다.
위의 예제처럼, 자신이 선언 된 환경(lexical scope)을 기억하는 함수를 Closure라고 부를 수 있겠습니다.
Closure 활용하기
그럼 이제부터 Closure의 여러 활용 예제들을 살펴보도록 하겠습니다.
은닉화
Closure를 통해 변수의 은닉화를 구현할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Counter() {
let count = 0;
// closure
this.increase = () => {
return ++count;
};
// closure
this.decrease = () => {
return --count;
};
}
const counter = new Counter();
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
count
변수가 this
에 바인딩 된 프로퍼티라면 외부에서도 접근가능한 public
프로퍼티가 됩니다.
하지만 위와 같이 Closure를 사용하여 count
변수를 바인딩 하면, 외부에서 count
변수에 접근할 수 없습니다. 이런 방식으로 JavsScript에서private
프로퍼티를 구현할 수 있습니다.
반복문에서의 var 문제 해결
1
2
3
4
5
6
7
8
9
const fns = [];
for (var i = 0; i < 4; i++) {
fns.push(() => {
console.log(i);
});
}
fns.forEach((fn) => fn()); // 출력 4 4 4 4
위 코드의 의도는 0 1 2 3 을 출력하는 것이지만 결과는 4 4 4 4 가 출력됩니다.
그 이유는 Closure가 바인딩 하는 것은 스코프의 reference 값이기 때문입니다. 즉 fns
배열에 저장 된 함수들이 호출 될 때, 그 함수들이 참조하는 lexical scope의 var 변수는 이미 4가 된 상태니까 4 4 4 4 가 출력되는 것이죠.
var는 함수 레벨 스코프를 따릅니다. 따라서 현재 var는 전역에 선언된 변수이고, fns 배열의 함수들이 호출될 때는 for문을 다 돌고 var 변수가 이미 4인 상태입니다.
이것을 해결하기 위해 Closure를 활용할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
const fns = [];
for (var i = 0; i < 4; i++) {
((i) => {
fns.push(() => {
console.log(i);
});
})(i);
}
fns.forEach((fn) => fn()); // 출력 0 1 2 3
위의 경우 즉시 실행 함수를 통해 매 반복마다 새로운 스코프를 생성하게 됩니다.
즉, fns
배열의 함수들은 즉시 실행 함수의 파라미터로 전달 된 i의 값을 참조하게 되므로 0 1 2 3이 정상적으로 출력됩니다.
또한, 해당 문제는 let
키워드를 사용해서 쉽게 해결할 수 있습니다. var
가 함수 레벨 스코프인 것과 달리 let
은 블록 레벨 스코프입니다. 따라서 for
문의 매 반복(블록) 마다 i
가 선언되고, scope가 생성됩니다. 따라서 정상적으로 0 1 2 3 이 출력됩니다.
1
2
3
4
5
6
7
8
9
const fns = [];
for (let i = 0; i < 4; i++) {
fns.push(() => {
console.log(i);
});
}
fns.forEach((fn) => fn()); // 출력 0 1 2 3
함수 합성
함수 합성도 결국 Closure를 활용한 것이라 볼 수 있습니다.
아래의 예제를 봅시다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
// closure
const add2 = (val) => add(val, 2);
// closure
const multiply3 = (val) => multiply(val, 3);
// closure
const add2AndMul3 = (val) => {
return multiply3(add2(val));
};
console.log(add2AndMul3(3)); // 15
위와 같이 간단하게 함수 합성을 만들어봤습니다.
add2
, multiply3
, add2AndMul3
함수 모두, 외부 함수의 렉시컬 스코프를 참조하여 val
변수를 사용하고 있습니다. 따라서 세 함수 모두 Closure 입니다.
pipe 함수 만들기
이제 pipe
함수를 사용하여 보다 직관적인 모습의 함수 합성을 구현해보겠습니다. pipe
함수 역시 closure라고 할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const pipe = (...funcs) => {
return (initialvalue) => {
return funcs.reduce((val, fn) => fn(val), initialvalue);
};
};
const add = (a, b) => a + b;
const add2 = (val) => add(val, 2);
const multiply = (a, b) => a * b;
const multiply7 = (val) => multiply(val, 7);
// closure
const add2AndMul7 = pipe(add2, multiply7);
console.log(add2AndMul7(3)); // 35
pipe
함수를 보시면 외부 함수의 파라미터인 funcs
를 사용하여 값을 계산하는 내부 함수를 return
하고 있습니다.
보시는 것처럼 pipe
함수를 통해 간단하게 함수 합성을 구현할 수 있습니다.
Conclusion
다양한 예제들을 통해 Closure의 활용 방법을 살펴봤습니다.
앞선 예제에서 보셨다시피, Closure를 사용함으로써 은닉화, 함수 합성 등의 다양한 기능을 구현할 수 있습니다.
하지만 Closure는 scope를 계속해서 유지함으로써 나타나는 메모리 누수 현상이 일어날 수 있습니다.
또한 스코프 체이닝 탐색으로 인한 퍼포먼스 문제도 일어날 수 있습니다.
따라서 Closure를 사용할 때는 메모리와 퍼포먼스 적인 부분에 대해 고민하면서, 적절히 사용하시는 것을 추천합니다.
Closure를 이해하는데 조그마한 도움이 되셨길 바라면서, 이만 줄이도록 하겠습니다.
감사합니다!
Reference
- https://poiemaweb.com/js-closure
- https://meetup.toast.com/posts/86
- https://blog.bitsrc.io/closures-in-javascript-why-do-we-need-them-2097f5317daf