클로저 closure

moreas·2023년 12월 27일
0

Javascript

목록 보기
5/6
post-thumbnail

🥚 클로저란 무엇인가

“A closure is the combination of a function and the lexical environment within which that function was declared.”
클로저함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다.

코어 자바스크립트에서 정의한 클로저에 대해 좀 더 알아보면

어떤 함수 A에서 선언한 변수 a를 참조하는 내부 함수 B외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상

그동안 여러 JS강의에서 들었던 클로저에 대한 설명을 종합해보자면,
함수 중첩 시 자식 함수가 부모 함수 범위에 접근 가능한 것, 즉 부모 함수 안에서 자식 함수를 선언하면 자식함수를 어디에서 호출하더라도 자식함수 안에서 부모함수의 변수에 접근할 수 있다고 설명한다.



클로저는 자바스크립트 고유의 개념이 아니다

함수형 프로그래밍 언어에서 사용되는 특성 중 하나이므로 ECMA Script에는 정의되어 있지 않다.

클로저에 대해 좀 더 명확하게 이해하기 위해서는 실행 컨텍스트에 대해 이해하는 것이 필요하다.



실행 컨텍스트 Execution Context


function outerFunc() {
  var x = 10;
  var innerFunc = function () { 
    console.log(x); 
  };
  innerFunc();
}

outerFunc(); // 10

내부함수 innerFunc가 호출되면 자신의 실행 컨텍스트가 실행 컨텍스트 스택에 쌓이고 변수 객체(Variable Object)와 스코프 체인(Scope chain) 그리고 this에 바인딩(실제 값 또는 프로퍼티를 확정)할 객체가 결정된다. 이때 스코프 체인은 전역 스코프를 가리키는 전역 객체와 함수 outerFunc의 스코프를 가리키는 함수 outerFunc의 활성 객체(Activation object) 그리고 함수 자신의 스코프를 가리키는 활성 객체를 순차적으로 바인딩한다. 스코프 체인이 바인딩한 객체가 바로 렉시컬 스코프의 실체이다.

내부함수 innerFunc가 자신을 포함하고 있는 외부함수 outerFunc의 변수 x에 접근할 수 있는 것, 다시 말해 상위 스코프에 접근할 수 있는 것은 렉시컬 스코프의 레퍼런스를 차례대로 저장하고 있는 실행 컨텍스트의 스코프 체인을 자바스크립트 엔진이 검색하였기에 가능한 것이다.



렉시컬 스코프 Lexical Scope (어휘적 범위)

스코프는 함수의 유효범위를 함수를 어디에서 실행했느냐가 아니라 어디서 선언(정의)되었느냐에 따라 결정된다. 함수는 동적이지만 함수 정의는 정적이기 때문이다. 이를 렉시컬 스코핑(Lexical scoping)라 한다.

정적 스코프 = 렉시컬 스코프 

위 예제의 함수 innerFunc는 함수 outerFunc의 내부에서 선언되었기 때문에 함수 innerFunc의 상위 스코프는 함수 outerFunc이다. 함수 innerFunc가 전역에 선언되었다면 함수 innerFunc의 상위 스코프는 전역 스코프가 된다.

  • 로컬에서 선언된 함수를 찾고
  • 없으면 전역 스코프로 이동해서 함수를 찾는다.



정리

function makeAdder(x){
 return function(y) { // y를 가지고 있고 상위함수인 makeAdder의 x에 접근 가능
	return x + y ; 
 }
}

const add3 = makeAdder(3);
console.log(add3(2)) // 5. 
// add3 함수가 생성된 이후에도 상위함수인 makeAdder의 x에 접근 가능 

const add10 = makeAdder(10);
console.log(add10(5)); // 15
console.log(add3(1)); // 4
  • 함수가 생성될 당시의 외부 변수를 기억하고 생성된 이후에도 그 변수에 계속 접근이 가능
  • 외부함수의 실행이 끝나서 외부함수가 소멸된 이후에도 내부함수가 외부함수의 변수에 접근할 수 있다.

=> 즉, 반환된 내부함수가 자신이 선언되었을 때의 렉시컬 스코프를 기억하여 자신이 선언되었을 때의 스코프 밖에서 호출이 되어도 그 환경에 접근할 수 있는 함수



🐣 클로저를 왜 사용하는가

정보의 은닉화 (접근 권한 제어)

직접적으로 변경하면 안되는 변수에 대해 접근을 막는 것을 은닉화(객체에서 속성을 직접 접근하지 못하게 숨기는 것)라고 한다.

function a(){
  let temp = 'a' 
  
  return temp;
} 


// console.log(temp)  error: temp is not defined
const result = a()
console.log(result); //a

현재 위 함수 내부적으로 선언된 temp에는 직접적으로 접근을 할 수 없다. 함수 a를 실행시켜 그 값을 result라는 변수에 담아 클로저를 생성함으로써 temp의 값에 접근이 가능하다.

  • 외부에서 쉽게 접근 가능한 로직
function Hello(name) {
  this._name = name;
}

Hello.prototype.say = function() {
  console.log('Hello, ' + this._name);
}

let a = new Hello('영서');
let b = new Hello('아름');

a.say() //Hello, 영서
b.say() //Hello, 아름

a._name = 'anonymous'
a.say() // Hello, anonymous
  • 클로저를 사용한 로직으로 변경
function hello(name) {
  let _name = name;
  return function () {
    console.log('Hello, ' + _name);
  };
}

let a = new hello('영서');
let b = new hello('아름');

a() //Hello, 영서
b() //Hello, 아름


전역 변수 사용을 억제 (지역 변수 보호)

  • 전역변수를 많이 선언하고 사용할 때 실수를 할 위험성이 있다. 몇 줄 안 되는 코드 내에서는 문제 원인을 쉽게 찾아 해결할 수 있으나 1억 개가 넘는 코드가 있다고 가정하면 클로저 사용을 통해 전역 변수에 잘못 접근하는 실수 자체를 줄일 수 있다.
  • 전역 변수를 사용한 로직
    : 잘 동작하지만 변수에 누구나 접근 가능하여 오류를 발생시킬 가능성을 내포하고 있는 코드다.
<script>
    var incleaseBtn = document.getElementById('inclease');
    var count = document.getElementById('count');

    // 카운트 상태를 유지하기 위한 전역 변수
    var counter = 0;

    function increase() {
      return ++counter;
    }

    incleaseBtn.onclick = function () {
      count.innerHTML = increase();
    };
  </script>
  • 지역 변수를 사용한 로직
    : increase 함수가 호출될 때마다 지역변수 counter를 0으로 초기화하기 때문에 언제나 1이 표시된다. 다시 말해 변경된 이전 상태를 기억하지 못한다.
 <script>
    var incleaseBtn = document.getElementById('inclease');
    var count = document.getElementById('count');

    function increase() {
      // 카운트 상태를 유지하기 위한 지역 변수
      var counter = 0;
      return ++counter;
    }

    incleaseBtn.onclick = function () {
      count.innerHTML = increase();
    };
  </script>
  • 클로저를 사용한 로직
    : 변수 increase에는 함수 function () { return ++counter; }가 할당된다. 이 함수는 자신이 생성됐을 때의 렉시컬 환경(Lexical environment)을 기억하는 클로저다. 즉시실행함수는 호출된 이후 소멸되지만 즉시실행함수가 반환한 함수는 변수 increase에 할당되어 inclease 버튼을 클릭하면 클릭 이벤트 핸들러 내부에서 호출된다. 이때 클로저인 이 함수는 자신이 선언됐을 때의 렉시컬 환경인 즉시실행함수의 스코프에 속한 지역변수 counter를 기억한다. 따라서 즉시실행 함수의 변수 counter에 접근할 수 있고 변수 counter는 자신을 참조하는 함수가 소멸될 때까지 유지된다.
<script>
    var incleaseBtn = document.getElementById('inclease');
    var count = document.getElementById('count');

    var increase = (function () {
      // 카운트 상태를 유지하기 위한 자유 변수
      var counter = 0;
      // 클로저를 반환
      return function () {
        return ++counter;
      };
    }());

    incleaseBtn.onclick = function () {
      count.innerHTML = increase();
    };
  </script>



최신 상태를 유지

  • 현재 상태를 기억하고 업데이트를 가능하게 한다.
  • useState도 클로저를 활용한 Hook이다.

useState는 setState를 통해 상태를 변경하고, 함수가 실행되었을 때 이전 상태를 기반으로 상태가 변경되며 항상 최신 상태를 유지한다.

실제로 컴포넌트에서 상태의 변경을 감지하기 위해서 함수가 실행되었을 때 이전 상태에 대한 정보를 가지고 있어야 한다. 그리고 이 과정에서 클로저를 사용한다.

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

useState의 내부 코드는 위와 같이 정의되어있다.
dispatcher에는 resolveDispatcher함수의 결과가 할당되고, dispatcher의 useState메서드에 초기값을 전달하고 반환된 값을 리턴한다.

function resolveDispatcher() {
  var dispatcher = ReactCurrentDispatcher.current;
  return dispatcher;
}

그리고 resolveDispatcher는 ReactCurrentDispatcher라는 객체의 current프로퍼티를 반환하는데
여기서 ReactCurrentDispatcher는 전역에 설정되어있는 객체이다.

따라서 useState가 반환한 상태의 배열은 전역객체로부터 오고 hook을 호출하면 클로저 동작에 의해 컴포넌트 밖에 있는 외부 스코프에 존재하는 state값에 접근, 참조할 수가 있다. 컴포넌트에서 값의 변경이 있으면 외부의 값이 변경되고 컴포넌트 내부에서 업데이트된다.



마무리

  • 간단하게 클로저에 대해 정리해보았다. 블로그에 포스팅을 하면서 몇몇 설명은 아직 100% 이해했다고 할 순 없지만 기계적으로 사용해왔던, js에서 가장 난해하다는 개념에 대해 정리해본 것만으로도 많은 발전이라고 생각한다. 심화적인 내용을 공부하면서 더 상세한 내용을 정리해보고 싶다.



출처

  • MDN - 클로저
  • 유튜브 생활코딩 클로저
  • 유튜브 라매 개발자 클로저
  • 유튜브 코딩앙마 자바스크립트
  • 유튜브 코드깎는노인 클로저
  • 책 자바스크립트 딥다이브
  • 클로저
  • 실행컨텍스트
profile
Everything is connected 🐶 좀 더 나은 개발을 위해

0개의 댓글