클로저(Closure)란? feat. Context, Lexical Scoping

Jonghyeon·2021년 8월 5일

JavaScript

목록 보기
1/1
post-thumbnail

먼저 클로저는 많은 라이브러리에서 사용됩니다.
클로저가 무엇인지 잘 이해하고 갑시다.

클로저(Closure)란?

클로저는 주위의 상태를 참조하는 내부 함수를 감싸는 외부함수를 말합니다. 클로저는 외부 함수의 스코프에 내부함수가 접근할 수 있는 권한을 줍니다. 함수가 만들어질 때 매번 클로저는 만들어집니다.

함수가 선언된 스코프를 기억하여 스코프 밖에서 실행될 때에도 기억한 스코프에 접근할 수 있게 만드는 문법이다.

function init() {
  var name = 'Mozilla'; // Local variable
  function displayName() { // Inner function, a closure
    alert(name); // use variable declared in the parent function
  }
  displayName();
}
init();

출처: MDN https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures#lexical_scoping

클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.

Lexical Scoping

스코프는 함수를 호출할 때가 아니라 선언할 때 생깁니다. 호출이 아니라 선언요! 정적 스코프라고도 불립니다.

다음 코드에서 console.log가 어떻게 찍힐 지 예상해봅시다.

var name = 'zero';
function log() {
  console.log(name);
}

function wrapper() {
  name = 'nero'; // 바꾼 부분
  log();
}
wrapper();

nero가 찍힙니다. log를 호출하기 전에 name을 'nero'로 바꿨거든요. 그럼 다음은요? 문제를 살짝 바꿨습니다.

var name = 'zero';
function log() {
  console.log(name);
}

function wrapper() {
  var name = 'nero'; // 바꾼 부분
  log();
}
wrapper();

똑같이 nero 아니냐고요? 땡! zero입니다. 스코프는 함수를 선언할 때 생긴다고 했죠? log 안의 name은 wrapper 안의 지역변수 name이 아니라, 전역변수 name을 가리키고 있는 겁니다.
이것을 Lexical scoping이라고 합니다. 함수를 처음 선언하는 순간, 함수 내부의 변수는 자기 스코프로부터 가장 가까운 곳(상위 범위에서)에 있는 변수를 계속 참조하게 됩니다.

클로저를 이용하면 Private method처럼 사용할 수 있다.

프라이빗 메소드는 코드에 제한적인 접근만 허용한다. 전역 네임 스페이스를 관리하는 강력한 방법을 제공하여 불필요한 메소드가 공용 인터페이스를 혼란스럽게 만들지 않도록 한다.

아래 코드는 처음보는 게 나왔는데요. (function() {})(); 구문입니다. IIFE(즉시 호출 함수 표현식, Immediately Invoked Function Expression)이라고도 하고, 모듈 패턴이라고도 하는데, 함수를 선언하자마자 바로 실행시켜버리는 거죠.



var counter = (function () {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };
})();

console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1

여기서 우리는 counter.increment, counter.decrement, counter.value 세 함수에 의해 공유되는 하나의 어휘적 환경을 만든다.

이런 방식으로 클로저를 사용하여 객체지향 프로그래밍의 정보 은닉과 캡슐화 같은 이점들을 얻을 수 있다. 그래서 이 구문이 라이브러리를 만들 때 기본입니다. 많은 라이브러리가 이 구문을 활용하고 있습니다. 비공개 변수가 없는 자바스크립트에 비공개 변수 기능을 만들어주기 때문이죠. 이 패턴은 꼭 기억하고 있어야합니다!

사용자 통제 방법

이런 방식으로 비공개 변수를 만들어 활용할 수 있습니다. 비공개 변수이기 때문에 남들이 조작할 걱정은 없죠. 프로그램 사용자는 여러분이 공개한 메소드만 사용해야합니다. 사용자가 예상을 뒤엎는 행동을 하는 것을 막을 수 있죠. 꼭 알아두어야 할 점은 절대로 사용자를 믿어서는 안된다. 무슨 짓을 할 지 모르거든요. 해킹을 시도할 수도 있고, 프로그램에 버그를 만들 수도 있습니다. 특히 서버와 연결되어 있는 경우는 더 조심해야하죠. 그렇기 때문에 항상 사용자가 할 수 있는 모든 행동과 일어날 수 있는 경우의 수를 통제하고 있어야 합니다. 자바스크립트에서 사용자를 통제하기 위한 기본적인 방법이 바로 클로저입니다.

단점: 성능, 메모리 문제 발생 가능성

closure의 비공개 변수는 자바스크립트에서 언제 메모리 관리를 해야할 지 모르기 때문에 자칫 메모리 낭비로 이어질 수 있습니다. 또한 scope chain을 거슬러 올라가는 행동을 하기 때문에 조금 느립니다.

클로저 스코프 체인

클로저에는 세가지 스코프가 있다

  • 지역 범위 (Local Scope, Own scope)
  • 외부 함수 범위 (Outer Functions Scope)
  • 전역 범위 (Global Scope)

루프에서 클로저 활용시 일반적인 실수

ECMAScript 2015에서 let 키워드가 소개되기 전, 루프안에서 클로저들을 만들 때 일반적으로 일어나는 문제가 있었습니다. 아래의 코드를 봅시다.

HTML 코드

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>

위의 HTML을 조작하는 JavaScript 코드

function showHelp(help) {
  document.getElementById('help').textContent = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

위 코드를 실행시켜보세요. JSFiddle에서 실행시켜보기

helpText 배열에 3가지 help 힌트들을 담고 있습니다. 각 힌트들은 Input필드의 아이디와 연관되어 있습니다. 루프는 이 배열의 길이 만큼 돌면서 각 Input필드의 onfocus이벤트에 추가 하려고 합니다.

이 코드를 실행해보면 알겠지만 기대대로 동작하지 않습니다. 어떤 Input필드에 focus하는 것과 상관없이 메시지는 그냥 age에 대한 힌트만 보여줄 것입니다.

그 이유는 onfocus에 할당된 함수들이 클로저이기 때문입니다. 함수의 정의와 setupHelp함수의 스코프를 캡쳐한 환경으로 구성되어 있습니다. 3개의 클로저들은 루프에서 만들어지지만 모두 달라지는 값을 가진 변수를 가진 lexical 환경을 공유합니다.

이것은 var로 선언된 변수 아이템이기 때문에 함수 스코프를 가지고 호이스팅 되기 때문입니다.

profile
종현

0개의 댓글