[JavaScript] Closure의 개념과 활용

Closure

Posted by dongjune on January 21, 2022

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