FEB:)DAIN

JavaScript Closure는 무엇일까? 본문

코딩/공부

JavaScript Closure는 무엇일까?

얌2 2023. 10. 19. 02:38
728x90

- 클로저를 설명하기에 앞서,

클로저는 하나의 어떤 것을 정의하는 개념이 아니기 때문에 사람마다 말하는 클로저가 다를 수 있다.

 

1. 함수의 주변 상태(렉시컬 환경)에 대한 참조를 같이 가지고 있는 함수는 클로저다. 즉 (new Function을 제외한) 모든 함수가 클로저라고 할 수 있다. (= 자바스크립트의 클로저)

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

MDN 문서에 따르면, 클로저는 함수의 주변 상태(렉시컬 환경)에 대한 참조를 같이 묶은(봉인한) 함수의 조합이다. 다시 말해 클로저는 내부 함수에서 외부 함수의 스코프에 접근할 수 있게 해 준다. 자바스크립트에서 클로저는 함수 생성 시점에, 함수가 생성될 때마다 만들어진다.

💡 함수 선언문 vs 함수 표현식 둘 중 어떤 걸 쓰느냐에 따라 함수 생성 시점이 다르다.

function(함수 선언문)을 사용하면 런타임 이전에 자바스크립트 엔진이 함수 선언과 동시에 
함수 객체를 생성해서 환경 레코드에 기록 → 코드 평가 단계에서 함수 생성

함수 표현식, 화살표 함수 표현식을 사용하면 런타임 시점에 자바스크립트 엔진이 함수를 읽을 때 
함수 객체를 생성해 환경 레코드에 기록 → 코드 실행 단계에서 함수 생성

따라서 여기서 말하는 클로저는 상위 렉시컬 환경(함수가 선언된 위치의 렉시컬 환경)을 참조하는 함수를 말한다. 자바스크립트에서 모든 함수는 생성될 때, 렉시컬 환경이 생성되고 이 렉시컬 환경은 상위 렉시컬 환경을 참조한다. 따라서 자바스크립트에서 new Function을 제외한 모든 함수를 클로저라고 할 수도 있는 것이다.

💡 new Function으로 생성한 함수는 상위 렉시컬 환경을 참조하지 못하고, 전역 렉시컬 환경만 참조할 수 있다.

 

2. 상위 렉시컬 환경이 아닌, 아래의 코드처럼 함수가 선언된 위치의 변수(외부 변수)를 참조하는 것을 클로저라고 하는 경우도 있다. (= 함수형 프로그래밍의 클로저)

function init() {
  var name = "Mozilla";
  
  function displayName() {
    console.log(name); // breakpoint
  }
  
  displayName();
}

init();

이 부분은 디버깅을 통해 눈으로 확인할 수도 있다.

displayName 함수는 생성과 동시에 렉시컬 환경이 생겼고, 이 렉시컬 환경은 외부 렉시컬 환경을 참조한다. 하지만 외부 변수를 참조하고 있지 않으므로 Scope에 Closure가 생기지 않는다. 다시 외부 변수를 참조하고 있는 코드를 확인해보자.

외부 변수인 name을 참조하고 있으므로 Closure가 생긴 걸 확인할 수 있다. 그리고 이 클로저에는 외부 변수인 name이 담겨져있다.

하지만 이런 의문이 생길 수 있다. displayName을 return 하지 않았는데 클로저라고 할 수 있나? 클로저는 상위 실행 컨텍스트가 콜 스택에서 빠졌음에도 불구하고 그 상위 실행 컨텍스트의 변수를 참조할 수 있는 거 아닌가?

 

3. 클로저는 상위 실행 컨텍스트가 콜 스택에서 빠졌음에도 불구하고 그 상위 실행 컨텍스트의 변수를 참조할 수 있는 것이 클로저의 본질이므로 클로저를 얘기할 때 이 정의를 말하는 경우가 많다.


 이제 클로저의 본질에 맞는 클로저 예시 코드를 살펴보자.

function makeFunc() {
  const name = "Mozilla";
  
  function displayName() {
    console.log(name);
  }
  
  return displayName;
}

const myFunc = makeFunc();
myFunc();

이렇게 클로저를 사용하면 makeFunc 함수가 실행되고 콜 스택에서 빠져도 displayName 함수(내부 함수)에서 makeFunc 함수(외부 함수)의 name에 접근할 수 있다.
 

여기서 잠깐


두 번째, 세 번째에서의 클로저 관점에서 봤을 때 '전역 변수를 함수 안에 사용해도 클로저'라고 착각하기가 쉬운데, 이는 클로저가 아니다. 개발자 도구 > Sources로 들어가 아래 fn1 함수 안 콘솔에 브레이크 포인트를 걸고 reload 시켜보자.

<script>
  let a = 1;
  
  function fn1() {
    let message = 'hi';
    
    console.log(a, message, message2); // breakpoint
  }
  
  function fn2() {
    let message2 = 'bye';
    
    fn1();
  }
  
  fn2();
</script>

전역에 변수를 선언하면 Script 스코프에 변수가 생긴다. ( a: 1 )
그리고 함수 fn1의 변수는 Local 스코프 안에 생긴다. ( message: 'hi' )

(번외) 자바스크립트는 정적 스코프(= lexical scope)를 채택하고 있기 때문에 fn2에서 fn1을 호출한다고 하더라도, fn1에서 fn2의 message2 변수에 접근할 수 없다. *정적 스코프는 함수가 어디에서 정의되느냐에 따라 유효 범위가 결정되고, 동적 스코프는 어디에서 호출되느냐에 따라서 접근할 수 있는 유효 범위가 정해진다.

하지만 전역 변수와 외부 함수 outer의 변수를 참조하고 있는 inner 함수의 콘솔에 breakpoint를 걸어서 실행시켜 보면, Closure (outer)라는 것이 생기는 걸 볼 수 있다.

<script>
  let a = 1;
  
  function outer() {
    let b = 2;
    
    function inner() {
      console.log('closure', a, b); // breakpoints
    }
    
    return inner;
  }
  
  const test = outer();
  test();
</script>

전역 변수를 사용하고 있는 함수(fn1)를 실행시켰을 때는 클로저가 생기지 않았는데, outer 함수 변수인 b를 사용하고 있는 inner 함수를 실행시켰을 때는 클로저가 생겼다. inner 함수를 실행시켰을 때 b가 inner의 로컬에 없으므로 그 상위(외부 함수)의 로컬에 접근한다.

💡 스코프(Scope)가 계층적으로 연결되어있는 것을 스코프 체인이라고 한다.
자바스크립트 엔진은 스코프 체인을 통해 상위 스코프의 변수를 참조한다.


부모 함수인 outer 함수에 b가 존재하므로 에러 없이 콘솔에 b까지 찍히는 것을 확인할 수 있다.

그리고 전역 변수 참조 함수는 세 번째에서 말하는 클로저에도 부합하지 않는다. 전역 실행 컨텍스트는 가장 마지막에 빠지기 때문이다. 

 

그렇다면 다시 첫 번째로 돌아가서, new Function으로 만든 함수를 전역에서 선언하고 실행하면 이것은 클로저일까? 아니라고 생각한다. new Function은 상위 렉시컬 환경을 참조할 수 없는 성질을 가졌다. 전역에서 선언하고 실행했기 때문에 전역 렉시컬 환경이 상위 렉시컬 환경이 된 것이지, 상위 렉시컬 환경을 참조할 수 있기 때문에 참조한 것이 아니기 때문이다.

 

클로저의 이론은 그만하고, 클로저의 활용 방법에 대해서 알아보자. (일반적으로 자바스크립트에서 클로저를 사용했다고 했을 때는 세 번째 클로저를 의미한다.) 클로저는 아래의 두 가지 상황을 원할 때 사용하면 된다.


1) 변수가 의도치 않게 변경되지 않도록 은닉
2) 특정 함수에게만 변수 변경을 허용

 

결론

클로저는 통용되는 단어라 상황에 따라 달라질 수 있다. 일반적으로 말하는 클로저는 세 번째, 즉 상위 실행 컨텍스트가 콜 스택에서 빠졌음에도 불구하고 그 상위 실행 컨텍스트의 변수를 참조할 수 있는 것을 말한다. 변수가 의도치 않게 변경되지 않도록 은닉하거나 특정 함수에게만 변수 변경을 허용하고 싶을 때 이 클로저를 활용할 수 있다.

 
 
 
 

 

728x90