자바스크립트 이론 - 실행 컨텍스트와 클로저

19319 단어 JavaScriptJavaScript

실행 컨텍스트

실행 컨텍스트는 실행 가능한 자바스크립트 코드 블록(함수 등)이 실행되는 영역 이다. 이 컨텍스트 안에는 실행에 필요한 여러가지 정보(인자 값, 내부 변수, 선언된 함수 등)를 담고 있다.

ECMAScript에서는 실행 컨텍스트가 형성되는 경우를 다음과 같이 정의한다.

  • 전역코드 실행
  • eval() 함수 실행되는 코드 실행
  • 함수 안의 코드 실행

현재 실행되는 컨텍스트에서 이 컨테스트와 관련 없는 실행 코드가 실행되면, 새로운 컨텍스트가 생성되어 스택에 들어가고 제어권이 그 컨텍스트로 이동된다.

예시

console.log('전역 실행 컨텍스트');

function func1() {
  console.log('func1 실행 컨텍스트 ');
}

function func2() {
  func1();
  console.log('func2 실행 컨텍스트 ');
}

func2();

// --console--
// 전역 실행 컨텍스트 생성
// func1 실행 컨텍스트 
// func2 실행 컨텍스트 

실행 컨텍스트 생성 과정

1. 활성 객체 생성
실행 컨텍스트가 생성되면 자바스크립트 엔진은 해당 컨테스트에서 실행에 필요한 여러가지 정보를 담을 활성객체를 생성한다. 변수객체라고 불리기도 한다.

2. arguments 객체 생성
매개변수를 생성한다.

3. 스코프 정보 생성
현재 컨텍스트의 유효 범위를 나타내는 스코프 정보를 연결리스트 형태로 실행 컨텍스트에 [[scope]] 프로퍼티로 생성한다.

4. 변수 생성
실행 컨텍스트 내부에서 사용되는 지역 변수와 함수의 생성이 이루어진다. 이 과정에서는 변수(undefined 선언)나 내부 함수를 단지 메모리에만 생성하고, 6번 해당코드 실행시에 초기화가 이루어진다. (함수 호이스팅 원인)

5. this 바인딩
this 키워드를 사용하는 값이 할당된다. this를 참조하는 객체가 없으면 전역 객체를 참조한다.

6. 코드 실행
변수의 초기화 및 연산, 또 다른 함수 실행이 이루어진다.

스코프 체인

실행 컨텍스트 생성과정 중 스코프 정보 생성과정에서 생성된 리스트인 [[scope]] 프로퍼티를 뜻한다. 이 스코프 체인에서 찾지 못한다면 선언되지 않았다고 판단한다. 이 스코프 체인은 자신이 생성된 실행 컨텍스트의 스코프체인을 기반으로 만들어진다.

예시

var value = 'value1';

function printFunc() {
  var value = 'value2';
  
  function printValue() {
    return value; 
  }
  
  console.log(printValue());
}

printFunc();  // value2

위의 코드에 스코프 체인을 살펴보자.

전역 실행 컨텍스트

변수객체 {
value: 'value1'
printFunc
this
[[scope]] = [전역객체]
}

⏬ printFunc 실행

printFunc 실행 컨텍스트

변수객체 {
value: 'value2'
printValue
this
[[scope]] = [전역객체, printFunc 변수 객체]
}

⏬ printValue 실행

printValue 실행 컨텍스트

변수객체 {
this
[[scope]] = [전역객체, printFunc 변수 객체, printValue 변수 객체]
}

-----


클로저

클로저는 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수를 뜻한다. 그리고 클로저가 참조하고 있는 변수를 자유변수라고 한다.

function outerFunc() {
  let freeX = '자유변수';
  
  return function() {
    console.log(`${ freeX }: 저는 살아있습니다!`);
  }
}

const closure = outerFunc();
// outerFunc 실행 컨텍스트가 끝났다.

closure();  // 자유변수: 저는 살아있습니다!

위의 코드에서 보면 outerFunc의 실행 컨텍스트가 끝났는 데도 불구하고, outerFunc의 내부 변수인 freeX는 여전히 존재하고 있다. 이 때 freeX가 자유변수이고, freeX를 참조하고 있는 closure 함수가 클로저이다.


특징

장점

  • 자바스크립트에서 private 키워드를 사용하는 것과 같은 장점을 얻을 수 있다. (캡슐화, 은닉화)
  • 클로저를 사용하면 캡슐화를 통해 불필요한 코드의 반복을 줄일 수 있다.

단점

  • 스코프 체인이 첫번째가 아닌 그 이후의 객체에 존재해 성능을 떨어트릴 수 있다.
  • 무차별적으로 사용되면 메모리 부담이 많아진다.

클로저의 활용

함수의 캡슐화

아래는 인사말을 만드는 간단한 코드이다.

const greeting = ['Hi!', 'I am', ''];

function getCompletedStr(name) {
  greeting[2] = name;

  return greeting.join(' ');
}

console.log(getCompletedStr('minsu'));  // Hi! I am minsu

위에서 greeting이라는 배열을 다른 함수에서 접근해서 값을 바꿀 수도 있고, 실수로 같은 이름의 변수를 만들어 버그가 생길 수도 있다. 이는 특히 다른 코드와 통합한다거나 라이브러리를 만들때 문제가 될 수 있다.

이것을 클로저를 통해 캡슐화하여 해결할 수 있다. greeting 배열은 자유변수로 다른 함수들이 접근 못하게 은닉화 시킨다.


const getCompletedStr = function() {
  const greeting = ['Hi!', 'I am', ''];
  
  return function(name) {
    greeting[2] = name;
    return greeting.join(' ');
  };

}();

console.log(getCompletedStr('minsu'));  // Hi! I am minsu

반복문

아래 코드는 대표적인 클로저 예제이다.

function count() {
  var i;
  for (i = 0; i < 10; ++i) {
    setTimeout(function() {
      console.log(i)
    }, 100)
  }
}

count();

숫자를 0부터 9까지 출력하고 싶지만, 10이 10번 출력된다. setTimeout()에 인자로 넘긴 익명함수는 모두 0.1초 뒤에 호출될 것이다. 그 0.1초 동안 이미 반복문은 모두 순회하면서 i는 10이 된 상태이고, 익명함수가 호출되면서 10이 저장된 i를 참조하기 때문이다.

클로저를 이용하여 해결해보자.

function count() {
  var i;
  for (i = 0; i < 10; ++i) {
    (function(arg) {           // arg => 자유변수
      setTimeout(function() {  // 클로저 익명함수
        console.log(arg);
      }, 100);
    })(i);
  }
}

count();

즉시실행함수를 이용해서 0부터 9까지 출력되는 코드이다. for문을 돌면서 즉시실행함수가 동작하고, setTimeout함수에 자유변수 arg를 참조하는 클로저(익명함수)를 인자로 넣어준다. 그러면 10개의 클로저는 i 변수와 관계없이 각각의 arg 자유변수를 참조하게 되어 제대로 0부터 9까지 출력하게 된다.


클로저의 성능

대부분의 클로저에서는 스코프 체인에서 뒤쪽에 있는 객체이 자주 접근하므로 성능을 저하시키는 이유로 지목되기도 한다. 게다가 클로저를 사용한 코드는 그렇지 않은 코드보다 메모리 부담이 많아진다.

그렇다고 클로저를 쓰지 않는 것은 자바스크립트의 강력한 기능 하나를 무시하고 사용하는 것과 다름이 없다. 그러니 클로저를 적절하게 사용하고, 사용하지 않는 클로저는 메모리 해제를 해주는게 좋다.

function person(name){
  return {
    getName : function (){
      return name;
    }
  }
}

var aiden = person('Aiden');
var luna = person('Luna');
 
console.log(aiden.getName());
console.log(luna.getName());

// 메모리를 release 시키기 위해 클로저의 참조를 제거함
aiden = null;
luna = null;

좋은 웹페이지 즐겨찾기