클로저 Closure

gigi·2022년 7월 11일
0

설명에 앞서 가볍게 예시를 하나 살펴보고 가자

function() {
  let a = 1;
  console.log(a); // 1
}    
console.log(a); // error

위의 코드를 오류없이 동작 시키려면 어떻게 해야할까?

let a = 1;
function() {
  console.log(a); // 1
}    
console.log(a); // 1

지역변수는 보통 function이 끝나게 되면 메모리에서 사라지기 때문에, 변수를 함수 외부로 빼 전역변수로 할당해주면 될 것이다. 그렇다면 우리는 지역변수를 계속 참조 할 수 없을까? 이를 closure가 답변해준다.

function outerFn() {
  let count = 1;

  function add() {
	count++;
  }
 
  function output() {
    console.log(count);
  }

  return {add, output};
}

let counter = outerFn();
counter.output(); // 1
counter.add();
counter.output(); // 2
counter.add();
counter.add();
counter.output(); // 4

위 코드에서처럼 count는 지역 변수임에도 불구하고 영속성을 갖는다. 간단히 설명하면 이게 클로져이다. JavaScript에서는 함수가 처음 선언 될 때 count가 hoisting 되고, 함수가 계속 존재하는 한 count도 지속되어진다. count는 outerFn scope에 속한다. add, output과 같은 inner scope에는 outer scope에 대한 참조가 있다. counter는 inner scope를 가리키는 변수다. counter가 지속되는 한 count또한 사라지지 않고 존재한다. 이를 다르게 말하면 특정 함수가 참조하는 변수들이 선언된 렉시컬 스코프(lexical scope)는 계속 유지되는데, 그 함수와 스코프를 묶어서 closure 라고한다.

closure 와 lexical environment가 어떻게 연관되는지

lexical environment (어휘적 환경)

  • 어휘적 환경은 특정 코드가 작성, 정의된 환경을 의미한다. 내가 사용하고자 하는 변수, 함수 등이 어떤 어휘적 환경에 속해 있는지에 따라 이용 가능한 변수가 달라지게 되는데, 어떤 변수나 함수의 값은 이를 '어떻게 호출했는지' 가 아니라, '어디에서 정의했는지' 즉 lexical scope가 어디인지에 따라서 결정된다.
function outer() {
  let a = 1;

  function inner() {
    console.log(a);
  }

  inner();
}

outer(); // 1
  • 위의 예시를 보면 outer()의 실행 결과는 1이다. outer 함수 내부에서 inner 함수를 호출하는데, inner함수에는 a가 없기 때문에 상위 스코프인 outer함수에서 a를 찾게 되는 것이다.
let a = 1;

function foo() {
  let a = 10;
  bar();
}

function bar() {
  console.log(a);
}

foo(); // 1 or 10
bar(); // 1
  • 함수의 상위 스코프를 결정하는데에는 두 가지 방법이 있다.

    1. 동적 스코프(dynamic scope)로 함수가 호출되는 시점에 따라 상위 스코프를 결정하게 되는 경우
    2. lexical scope로 함수를 어디서 정의하였는지에 따라 상위 스코프를 결정하게되는 경우
  • 따라서 위의 예시의 경우

    1. 동적 스코프(dynamic scope) 라면 foo()의 실행 결과는 10
    2. lexical scope라면 foo()의 실행 결과는 1이 나오게 된다.
대부분의 프로그래밍 언어는 렉시컬 스코프이다.
function outer() {
  let name = "kimcoding";

  function inner() {
    console.log(`hi ${name}!`); // 'hi kimcoding!'
  }

  inner();

  return inner;
}

let greeting = outer();
greeting();
  • 한가지 예시를 더 보면 outer함수 내부에서 inner함수를 호출했을 때, lexical scope에 따라서 inner함수의 상위 스코프는 outer함수가 된다. 따라서 outer함수에 있는 name이라는 변수에 접근을 할수 있게 된다.
  • greeting 이라는 변수에는 outer함수의 리턴값인 inner함수가 담겨있다. outer 함수는 이미 종료되어 콜스택에서 빠져 나갔지만, greeting()을 실행해보면 여전히 name 이라는 변수에 접근해 hi kimcoding!을 찍는 것을 확인할 수 있다.
  • 이처럼 어떤 함수를 lexical scope 밖에서 호출해도, 원래 선언이 되었던 lexical scope를 기억하고 접근할 수 있도록 하는 특성을 closure라고 부른다.

클로저를 너무 남발하면?

일반적으로 함수가 실행될 때 생성된 컨텍스트는 함수가 종료될 때 가비지컬렉션의 수집대상이 되어 사라진다. 하지만 클로져 패턴이 사용된 경우에는 내부함수의 변수가 언제 외부함수의 변수를 참조할지 알 수 없기 때문에 외부함수가 종료되어도 가비지 컬렉션의 수집대상이 되지않고 메모리상에 남아있게 된다. 이런 이유로 클로저 패턴을 남발하게 되면 메모리 누수가 발생하고 이로 인해 퍼포먼스 저하가 일어날 수 있다.

0개의 댓글