Closure

자바스크립트에 관심이 있거나 공부한다면 한번쯤 들어봤을 개념이다.

클로저가 그럼 무엇일까?

  • execution context(실행컨텍스트)에 대한 사전 지식이 있으면 이해하기 어렵지 않은 개념이다. 클로저는 자바스크립트 고유의 개념이 아니라 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어(Functional Programming language: 얼랭(Erlnag), 스칼라(Scala), 하스켈(Haskell), 리스프(Lisp)…)에서 사용되는 중요한 특성이다. (PoiemaWeb 참조)
  • 클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다. (MDN 참조)

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

이렇다고 한다. 말이 너무 어렵다. 실행 컨텍스트는 뭐지. 렉시컬 스코프는 또 뭐지??

실행컨텍스트(execution context)?

자바스크립트에서 젤 중요한 개념이다.

개념

  • 실행할 코드에 제공할 환경 정보들을 모아놓은 객체
  • 자바스크립트의 동적 언어로서의 성격을 가장 잘 파악할 수 있는 개념

구성

실행 컨텍스트는 다음과 같은 것들을 이용하면 콜스택(call stack)에 쌓이게 된다.

  • 전역공간은 자동으로 컨텍스트로 구성된다.
  • 함수를 실행한다.
  • eval()함수를 실행한다.
  • block을 만든다 (ES6+)

잠깐만 그럼 콜스택은 뭐지??

자바스크립트 엔진

  • 가장 대중적인 자바스크립트의 엔진은 구글의 V8 엔진입니다. V8 엔진은 크롬과 노드 안에서 동작합니다.

자바스크립트 엔진은 다음과 같은 두 가지 주요 구성 요소로 이루어져 있습니다.

  • 메모리 힙(Memory Heap) — 객체는 힙, 대부분 구조화되지 않은 메모리 영역에 할당된다. 변수와 객체에 대한 모든 메모리 할당은 여기서 발생한다.
  • 호출 스택(Call Stack) — 코드가 실행될 때 호출 스택이 쌓인다.

실행 환경

  • 브라우저에는 개발자가 사용하는 거의 모든 API가 있다. 이 API에는 DOM, AJAX, setTimeout 등 브라우저에서 제공하는 API라고 하는 것들이 있다.

자바스크립트는 단일 스레드 프로그래밍 언어이다. 그렇기 때문에 단일 호출 스택이 있는데 단일 호출 스택이라 함은 한 번에 하나의 일만 처리할 수 있다는 말이다.

호출 스택의 구조

  • 함수를 실행하면 해당 함수의 기록을 스택 맨 위에 추가(Push) 합니다. 우리가 함수를 결과 값을 반환하면 스택에 쌓여있던 함수는 제거(Pop) 됩니다.
function multiply(x, y) {
    return x * y;
}
function printSquare(x) {
    var s = multiply(x, x);
    console.log(s);
}
printSquare(5);

그렇다. 자바스크립트는 대충 이런 원리로 실행된다.

그렇다면 다시 돌아와서

실행 컨텍스트(execution context)란 ?

var a = 1; // 전역 컨텍스트
function outer () { // outer 컨텍스트
  function inner () { // inner 컨텍스트
    console.log(a); // undefined
    var a = 3;
    console.log(a); // 3
  }
  inner();
  console.log(a); // 1
}
outer();
console.log(a); // 1

위와 같이 코드를 구성했을 때 실행 컨텍스트의 스택은 다음과 같은 순서로 실행된다.

  • 프로그램 실행: [전역컨텍스트]
  • outer 실행: [전역컨텍스트, outer]
  • inner 실행: [전역컨텍스트, outer, inner]
  • inner 종료: [전역컨텍스트, outer]
  • outer 종료: [전역컨텍스트]

그리고 이러한 정보들이 생성된다.

  • VariableEnvironment
    • 현재 컨텍스트 내의 식별자(변수)들에 대한 정보
    • 외부 환경 정보
    • 선언 시점의 LexicalEnvironment의 스냅샷(변경사항 반영 X)
  • LexicalEnvironment
    • 처음에는 VariableEnvironment와 같음
    • 변경 사항이 실시간으로 반영됨
  • ThisBinding
    • 식별자가 바라봐야 할 대상 객체

Variable Environment

  • VariableEnvironment에 담기는 내용은 LexicalEnvironment와 같지만, 최초 실행 시의 스냅샷을 유지한다. 실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 복사해서 LexicalEnvironment를 만든다.
  • 주로 활용하는 것은 LexicalEnvironment이다. 즉, VariableEnviroment는 스냅샷 유지를 목적으로 사용한다.

Lexcial Environment

  • LexicalEnvironment의 내부에는 environmentRecord와 outerEnvironmentReference로 구성돼 있다.
  • environmentRecord로 인하여 호이스팅이 발생한다.
  • outerEnvironmentReference로 인하여 스코프와 스코프체인이 형성된다.

environmentRecord와 Hoisting(호이스팅)

자바스크립트는 코드를 실행하기전에 식별자를 수집한다.

environmentRecord

현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다.

  • 매개변수 식별자
  • 함수 자체
  • 함수 내부의 식별자

Host Object(호스트 객체)

  • 전역 실행 컨텍스트는 변수 객체를 생성하는 대신 전역 객체를 활용한다.
  • 브라우저의 Window 객체, Node의 Global 객체 등이 이에 해당한다.
  • 이들은 Host Object로 분류된다.

즉, 코드가 실행 되기 전에 자바스크립트의 엔진은 이미 실행 컨텍스트에 속한 변수명들을 모두 알고 있게 되는 셈이다.

개발자 황준일님 감사합니다.. 블로그 글 보고 공부 많이 했어요..

호이스팅이란?

  • 자바스크립트 엔진이 실행 컨텍스트를 구성할 때 environmentRecord 에 식별자의 정보를 수집한다.
    이러한 과정을 통해 엔진은 함수를 실행하기도 전에 해당 컨텍스트 내부의 변수명들을 이미 알고있다.

이렇기에 ' 식별자들을 코드의 최상단으로 끌어올린다.' 라는 호이스팅이라는 개념이 생겨나는데, 물리적으로 끌어올린 것이 아닌, 실행 컨텍스트 관점에선 이미 식별자들의 정보를 알고 있으니 식별자 정보를 수집하는 과정을 이해하기 쉬운 방법으로 나타낸 추상화한 가상 개념이다.

실행 컨텍스트는 여기 까지 알아보도록 하자

그렇다면 멀리 돌아왔는데 렉시컬 스코프는 뭐지??

  • 함수를 어디서 호출하는지가 아니라 어디에 선언하였는지에 따라 결정되는 것을 말한다.
  • 즉, 함수를 어디서 선언하였는지에 따라 상위 스코프를 결정한다는 뜻이며, 가장 중요한 점은 함수의 호출이 아니라 함수의 선언에 따라 결정된다는 점이다.
  • 다른 말로, 정적 스코프(Static scope)라 부르기도 하다.

근데 그럼 스코프란!?

var x = 'global';

function foo () {
  var x = 'function scope';
  console.log(x);
}

foo(); // ? ->function-scope
console.log(x); // ? -> global
  • 스코프는 참조 대상 식별자(identifier, 변수, 함수의 이름과 같이 어떤 대상을 다른 대상과 구분하여 식별할 수 있는 유일한 이름)를 찾아내기 위한 규칙이다. 자바스크립트는 이 규칙대로 식별자를 찾는다.
  • 위 예제에서 전역에 선언된 변수 x는 어디에든 참조할 수 있다. 하지만 함수 foo 내에서 선언된 변수 x는 함수 foo 내부에서만 참조할 수 있고 함수 외부에서는 참조할 수 없다. 이러한 규칙을 스코프라고 한다.

스코프의 구분

전역 스코프 (Global scope)

  • 코드 어디에서든지 참조할 수 있다.

지역 스코프 (Local scope or Function-level scope)

  • 함수 코드 블록이 만든 스코프로 함수 자신과 하위 함수에서만 참조할 수 있다.

모든 변수는 스코프를 갖는다. 변수의 관점에서 스코프를 구분하면 다음과 같이 2가지로 나눌 수 있다.

전역 변수 (Global variable)

  • 전역에서 선언된 변수이며 어디에든 참조할 수 있다.

지역 변수 (Local variable)

  • 지역(함수) 내에서 선언된 변수이며 그 지역과 그 지역의 하부 지역에서만 참조할 수 있다.

자바스크립트는 함수 레벨 스코프(function-level scope)를 따른다. 함수 레벨 스코프란 함수 코드 블록 내에서 선언된 변수는 함수 코드 블록 내에서만 유효하고 함수 외부에서는 유효하지 않다(참조할 수 없다)는 것이다.

단, ECMAScript 6에서 도입된 let keyword를 사용하면 블록 레벨 스코프를 사용할 수 있다.
var x = 'global';

function foo() {
  var x = 'local';
  console.log(x);
}

foo();          // local
console.log(x); // global

전역변수 x와 지역변수 x가 중복 선언되었다. 전역 영역에서는 전역변수만이 참조 가능하고 함수 내 지역 영역에서는 전역과 지역 변수 모두 참조 가능하나 위 예제와 같이 변수명이 중복된 경우, 지역변수를 우선하여 참조한다.

다음은 함수 내에 존재하는 함수인 내부 함수의 경우를 살펴보자

var x = 'global';

function foo() {
  var x = 'local';
  console.log(x);

  function bar() {  // 내부함수
    console.log(x); // ?
  }

  bar();
}
foo();
console.log(x); // ?

또한 중첩 스코프의 경우 가장 인접한 지역을 우선 참조한다.

var foo = function ( ) {

  var a = 3, b = 5;

  var bar = function ( ) {
    var b = 7, c = 11;

// 이 시점에서 a는 3, b는 7, c는 11

    a += b + c;

// 이 시점에서 a는 21, b는 7, c는 11

  };

// 이 시점에서 a는 3, b는 5, c는 not defined

  bar( );

// 이 시점에서 a는 21, b는 5

};

근데 갑자기 띠용한게 있다.

var x = 1;

function foo() {
  var x = 10;
  bar();
}

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

foo(); // ?
bar(); // ?

결과를 에측해보자.
나는 10,1로 예측했다. 근데 결과는 !? 1, 1 이다. 대체 왜?

렉시컬 스코프

  • 렉시컬 스코프는 함수를 어디서 호출하는지가 아니라 어디에 선언하였는지에 따라 결정된다. 자바스크립트는 렉시컬 스코프를 따르므로 함수를 선언한 시점에 상위 스코프가 결정된다.

  • 함수를 어디에서 호출하였는지는 스코프 결정에 아무런 의미를 주지 않는다. 위 예제의 함수 bar는 전역에 선언되었다. 따라서 함수 bar의 상위 스코프는 전역 스코프이고 위 예제는 전역 변수 x의 값 1을 두번 출력한다.

알아두면 좋은 개념

즉시 실행 함수(IIFE)

전역변수 사용을 억제하기 위해, 즉시 실행 함수(IIFE, Immediately-Invoked Function Expression)를 사용할 수 있다.
이 방법을 사용하면 전역변수를 만들지 않을 수 있다.

즉시실행함수를 왜 사용할까?

  1. 필요없는 전역 변수의 생성을 줄일 수 있다.
    함수를 생성하면 그 함수는 전역 변수로써 남아있게 되고, 많은 변수의 생성은 전역 스코프를 오염시킬 수 있다.
  • 즉시실행함수를 선언하면 내부 변수가 전역으로 저장되지 않기 때문에 전역 스코프의 오염을 줄일 수 있다.
  1. private한 변수를 만들 수 있다.
    즉시실행함수는 외부에서 접근 할 수 없는 자체적인 스코프를 가지게된다. 이는 클로저의 사용 목적과도 비슷하며 내부 변수를 외부로부터 private하게 보호 할 수 있다는 장점이 있다.

클로저를 공부하려고 했는데 실행컨텍스트부터 콜스택, 엔진, 스코프, 렉시컬 스코프, 호이스팅, 즉시실행함수 등등 갑자기 어려운 개념들이 나온다. 정말 멀리 돌아왔지만 다음편에서는 이제 진짜 클로저에 대해 시작해보자

profile
프론트? 백? 초보 개발자의 기록 공간

0개의 댓글