본문 바로가기
Javascript

[Javascript] Closure, 그리고 IIFE의 활용

by kmmguumnn 2018. 5. 3.

이번에는 지난 포스트에서 소개한 Scope에 이어서, 역시 Javascript에서 자주 쓰이는 개념인 Closure에 대해 알아 보고, 추가로 IIFE(Immediately-Invoked Function Expressions)까지 정리해보도록 한다.


Closure (클로저)

closure를 이해하기 위해, 이렇게 생각해보자. 

함수의 scope에 대한 접근(access) 권한이 일회성이 아니라 계속 유지될 수 있다.

아직은 감이 잘 오지 않는다. 다음 코드에서 remember() 함수에 주목해 보자:

function remember(number) {
    return function() {
        return number;
    }
}

const returnedFunction = remember(5);

console.log( returnedFunction() );
// 5

remember() 함수 호출을 통해 Javascript 엔진이 remember() 함수에 접근하면, 앞선 실행 scope를 다시 가리키는 새로운 실행 scope를 만든다. 이 새로운 scope는 'number' 매개변수를 가리키는 참조를 포함한다(이 경우 숫자 5). 엔진이 내부 함수에 접근하면(함수 표현식), 현재 실행 scope에 링크를 연결한다.


함수가 자신의 scope로 접근을 제한하는 이러한 과정을 우리는 closure(클로저)라고 부른다. 이 예에서, 내부 함수는 'number'를 닫는다. closure는 매개변수의 어떤 숫자나 변수를 capture, 즉 붙잡아 둘 수 있다. MDN 문서를 보면 closure를 다음과 같이 정의하고 있다.

"the combination of a function and the lexical environment within which that function was declared."

즉, closure는:

  • 함수 그 자체, 그리고
  • 함수가 선언된 곳의 코드(더 중요하게는 scope chain)

함수가 선언되면, 함수는 scope chain에 고정된다. 여기서 재미있는 사실은, 함수가 scope chain을 계속 유지하고 있다는 것인데, 함수가 선언된 곳이 아닌 다른 곳에서 호출된다 하더라도 이것은 계속 유지된 채로 있다.


위의 함수를 다시 살펴보자. remember(5)가 실행되고 리턴된 다음, returnedFunction은 어떻게 'number'의 값에 접근할 수 있는 것일까? 실행되고 리턴까지 다 된 이후인데도 어떻게 가능할까? closure 덕분인데, closure는 함수 객체가 생성된 그 시점의 snapshot을 저장하게끔 해준다. 이제 본격적으로 closure에 대해 알아보자. closure는 앞서 살펴본 scope와도 밀접한 관련이 있다.


클로저 생성하기


새로운 함수가 정의될 때마다, 해당 함수를 위한 closure가 같이 생성된다. 즉 모든 함수는 closure를 갖는다. closure가 쓸모있는 상황은 주로, 중첩된 함수, 즉 함수 안에 새로운 함수가 정의되는 경우이다.

중첩된 함수는 그 바깥의 변수까지도 접근할 수 있다는 점을 떠올려 보자. 이는 안쪽의 함수를 둘러싸는 부모 함수의 변수까지도 포함한다는 뜻이다. 즉 중첩된 함수(안쪽 함수)는 자신의 인자로 받아지거나 지역 변수로 선언되지 않은 변수들(free variable이라고도 한다)까지도 접근 가능한 것이다.

앞서 remember() 함수에서 직접 알아본 것처럼, 함수는 자신의 부모 scope까지의 참조를 계속 유지한다. 만약 함수로의 접근이 가능하다면, scope 역시 지속되는 것이다.



Closure와 Scope


closure와 scope는 아주 밀접하게 연관되어 있어서, 같이 쓰이는 모습을 앞으로 자주 보게 될 것이다. 아래의 코드를 보자.

const myName = 'Kim';

function introduceMyself() {
  const you = 'student';

  function introduce() {
    console.log(`Hello, ${you}, I'm ${myName}!`);
  }

  return introduce();
}

introduceMyself();
// 'Hello, student, I'm Kim!'

'myName'은 global scope에 있는 global variable(전역변수)이다. 따라서 모든 함수에서 사용 가능하다.

'you'는 introduce()에서 참조하고 있는데, 'you'는 introduce()에서 선언되지 않은 변수다. 앞서 말했듯 이는 안쪽 함수의 scope는 감싸고 있는 함수의 scope에서 선언된 변수를 포함하기 때문에 가능한 것이다.

결론적으로, introduce() 함수와 lexical environment는 closure를 만들어 낸다.



Garbage Collection


Javascript는 garbage collection을 통해 메모리를 관리한다. 어떤 데이터에 대해 어느 누구도 참조하지 않는 상황이 되면, 그 데이터는 어느 순간 garbage collected, 즉 사라지게 된다.

closure의 관점에서 garbage collection을 생각해 보자. 부모 함수(감싸는 함수)의 변수들은 안쪽 함수에서도 사용할 수 있다는 것을 배웠다. 만약 안쪽 함수에서 부모의 변수들을 사용한다면, 그 변수들은 메모리에 위치하게 된다. (함수에 대한 참조가 계속 유지된다는 가정 하에)

즉 Javascript에서, 참조 가능한 변수들은 garbage collected되는 대상에서 제외된다. 다음의 코드를 보자.

function myCounter() {
  let count = 0;

  return function () {
    count += 1;
    return count;
  };
}

안쪽 함수가 존재함으로써, 'count' 변수는 garbage collection의 대상이 아니게 된다. 안쪽 함수가 'count' 변수를 참조하고 있기 때문이다.





IIFE (Immediately-Invoked Function Expressions)

("이피"라고 읽는다)


IIFE에 대해 본격적으로 시작하기 전에, "function declaration"과 "function expression"의 차이를 생각해 보자.


function declaration은 함수를 정의하고, 함수를 할당할 변수를 만들지 않는다. 따라서 함수 자신이 어딘가로 리턴될 일도 없다.

function returnHello() {
  return 'Hello!';
}

반면 function expression은 함수가 변수로 할당될 수 있다. 또한 함수가 익명일 수도 있고, 다른 함수 표현식의 일부가 될 수도 있다.

// anonymous
const myFunction = function () {
  return 'Hello!';
};

// named
const myFunction = function returnHello() {
  return 'Hello!';
};


Immediately-Invoked Function Expressions: 구조와 문법


IIFE는 정의되자마자 호출되는 함수다. 다음의 코드를 보자.

(function sayHi(){
    alert('Hi there!');
  }
)();

// alerts 'Hi there!'

좀 이상하게 생기긴 했는데, 그냥 함수를 괄호로 묶은 다음 그 바로 뒤에 한 쌍의 괄호를 추가해주면 끝이다.



IIFE로 인자 전달하기


IIFE에 인자를 전달해보자. 다음 익명 함수는 1개의 인자를 받는다.

(function (name){
    alert(`Hi, ${name}`);
  }
)('Kim');

// alerts 'Hi, Kim'

함수를 감싼 뒤에 따라 붙는 두번째 괄호쌍은, 그 앞의 함수를 즉시 실행시킬 뿐 아니라 인자를 받는 역할도 한다. 인자로 'Kim'을 받아서, 'name' 변수에 저장한다. 함수가 즉시 실행되면서, 'Hi, Kim' alert 창이 뜬다.

2개의 인자를 받는 예시도 살펴보자.

(function (x, y){
    console.log(x * y);
  }
)(2, 3);

// 6


IIFE의 private scope


IIFE의 주요 사용 용도는, private scope(private state)를 만들기 위함이다. Javascript에서 변수는 함수에 의해 scope가 정해진다는 것을 이미 알고 있다. 이를 활용해서, 변수나 메소드가 외부로부터 접근되는 것을 closure가 보호하도록 만들 수 있다. myFunction 함수에 의해 참조되는 IIFE의 안쪽에 있는 간단한 closure를 살펴보자.

const myFunction = (
  function () {
    const hi = 'Hi!';
    return function () {
      console.log(hi);
    }
  }
)();

myFunction을 자세히 분석해 보자.


myFunction은 지역변수 'hi'와, 'hi'를 capture하고 console에 출력하는 리턴된 함수로 구성된 IIFE를 가리키고 있다.


위의 경우에, IIFE는 함수를 즉시 실행하는 데 사용되고 있다. 

또한 myFunction은 private한, 즉 함수 바깥에서는 접근할 수 없는 state를 유지할 수 있도록 한다. 즉 IIFE가 코드를 감싸고 있어서, 내부의 코드가 global scope를 더럽히는 것을 방지할 수 있다. (private scope를 생성)



IIFE, Private Scope, 그리고 Event Handling


IIFE의 또다른 예시를 보자. 이번엔 event를 다루는 측면에서 생각해 보자.

어떤 버튼이 있을 때, 버튼을 클릭할 때마다 그 버튼을 몇 번 클릭했는지 횟수를 alert으로 띄워준다고 가정하자. 이를 위해 버튼이 클릭된 횟수를 계속 추적해야 할 것이다. 하지만, 횟수라는 숫자 데이터를 어떻게 유지해야 할까?

전역 변수로 횟수를 의미하는 'count' 변수를 만든 뒤, 그 변수를 계속 참조할 수도 있겠지만, 클릭 event handler 자체에 해당 데이터를 넣을 수 있다면 더 좋을 것이다.

이렇게 하면, 추가적인 변수로 global scope를 더럽히지 않게 될 뿐만 아니라, 만약 IIFE를 적용하면 closure를 조정하여 'count' 변수를 외부에서 참조하지 못하도록 보호할 수도 있을 것이다! 'count' 데이터의 우연한 수정이나 의도치 않은 side-effect를 사전에 방지하는 것이다.

이를 위해 간단한 HTML 파일을 만든다고 하자.

<!-- button.html -->

<html>

  <body>

     <button id='button'>Click me!</button>

     <script src='button.js'></script>

  </body>

</html>

id가 button인 button 태그가 있다. 이제 button.js 파일에서, 이 태그를 참조하는 'button' 변수를 만든다.

// button.js

const button = document.getElementById('button');

다음으로, 'button'에 클릭 이벤트에 대한 event listener를 추가하고, listener의 두번째 인자로 IIFE를 넘겨준다.

// button.js

button.addEventListener('click', (function() {
  let count = 0;

  return function() {
    count += 1;

    if (count === 2) {
      alert('This alert appears every other press!');
      count = 0;
    }
  };
})());

먼저, 지역 변수인 'count'를 선언하고 초기값으로 0을 할당했다. 다음으로 함수를 리턴하는데, 이 함수는 'count'를 증가시키면서 2에 도달하면 alert을 띄운 뒤 0으로 다시 초기화한다.

중요한 부분은, 리턴된 함수가 'count' 변수를 capture한다는 점이다. 즉 함수가 부모 scope에 대한 참조를 유지하므로, 'count' 변수는 리턴된 함수에서 이용할 수 있는 것이다. 결과적으로, 안쪽 함수를 리턴하는 바깥 함수를 즉시 실행하고, 리턴된 함수는 'count'에 접근할 수 있으므로 private scope가 생성되는 것이다. 즉 데이터를 보호할 수 있게 된다.

closure에 'count'를 담아 놓음으로써, 매번 클릭이 발생할 때마다 데이터를 유지할 수 있게 된다.



IIFE의 이점


IIFE가 어떻게 private scope를 만들고 변수와 메소드를 외부 접근으로부터 보호하는지 살펴 보았다. IIFE는 궁극적으로, closure 안의 private data에 접근하는 용도로 안쪽 함수를 사용한다. 이 안쪽 함수는 public하게 접근할 수 있으면서도, 그 내부에 정의된 변수의 privacy는 계속 유지되는 것이다.

또 다른 IIFE의 장점은, 전역 변수를 추가하지 않고서도 원하는 코드를 실행할 수 있다는 점이다. 하지만 IIFE는 특정한 실행 context를 생성하기 위해 오직 한 번 실행되는 의도로 사용된다는 점에 유의해야 한다. 만약 어떤 코드를 재사용하고자 한다면, IIFE보다는 함수를 선언하고 호출하는 일반적인 방식이 더 나을 것이다.

종합하면, 한 번만 수행할 일(예를 들어 app 초기화 작업)이 있다면, IIFE를 통해 global environment를 오염시키지 않으면서 원하는 일을 효과적으로 수행할 수 있을 것이다. global namespace를 깨끗하게 유지하는 것은 변수 이름의 중복과 같은 충돌의 위험을 줄여준다.







【 추가 자료 】






댓글