GRAMMAR

  • account_tree
  • bug_report

Scope vs Context

자바스크립트에서 유효범위(scope)와 컨텍스트(context)에 대한 개념은 매우 중요하다.

유효범위(scope)와 컨텍스트(context)는 비슷해 보인다. 스코프(scope)는 변수와 함수의 유효한 사용(접근)에 대한 개념을 다루고 컨텍스트(context)는 코드의 실행 단위에 대한 개념을 다룬다.

스코프(scope)

스코프(scope)란 무엇인가? 사전적인 의미는 범위를 뜻한다. 자바스크립트에서는 실행에 필요한 코드 단위가 만들어지고 변수가 선언되어 사용되는 규칙이라고 본다. 컨텍스트(context)에는 기본적으로 함수 단위의 스코프(scope)가 존재한다. 그리고 실행 컨텍스트에는 많은 변수와 함수가 존재한다. 이 변수와 함수들은 필요에 따라 사용되고 호출된다.

컨텍스트(context)는 코드 실행 시점에 엔진에 의해 만들어지는 실행 단위라고 볼 수 있다. 

함수 단위의 공간은 다른 함수가 포함되어 중첩 구조를 가질 수 있다. 함수 내에서 선언된 변수는 계층 구조에 따라 접근이 가능하거나 불가능해진다. 함수 또한 계층 구조를 가지면서 호출할 수 있는 위치가 정해지기도 한다.

이렇게 변수의 사용 범위와 함수의 사용 범위를 나눈 것을 스코프(scope)라고 한다. 기본적으로 스코프(scope)는 전역 스코프(global scope)와 함수 스코프(function scope)로 구분된다. 그리고 ES6부터는 블록 스코프(block scope)가 추가되었다. 또한 지금까지는 선언 시점에서 정해진 스코프(scope)이지만 실행 단계에서도 동적 스코프(dynamic scope)가 만들어진다. 선언 시점의 스코프를 렉시컬 스코(lexical scope)라 하고 실행 단계에서 동적으로 생성된 스코프(scope)를 동적 스코프(dynamic scope)라고 부른다.

코드가 실행이 되면 컴파일러는 다양한 상황에 따라 스코프(scope)를 나누어 스코프 컬렉션(scope collection)에 담는다.  스코프 컬렉션(scope collection)에 담겨져 탐색 대상이 되는 스코프(sceop)들을 스코프 체인(scope chain)이라고 부른다.

이 글에서는 자바스크립트에서 다루는 스코프(scope)에 대한 전반적인 개념을 살펴보는것에 한정한다.

보통 스코프(scope)에서 다루는 대상은 변수이며 변수의 사용 가능 여부를 결정 짓는다.

전역 스코프(global scope)

전역 스코프(global scope)는 웹 문서내에서 <script>태그로 선언된 기본적인 공간을 의미한다. 전역 스코프 내에서는 변수와 함수가 선언된다. 특히 함수가 선언되면 함수 영역은 별개의 지역 스코프로 생성된다.

전역 스코프에 선언된 변수는 모든 지역 스코프에서 접근이 가능하다. 전역 스코프는 Window 라는 전역 객체가 존재하며 전역 공간에 선언된 변수와 함수는 Window 전역 객체의 프로퍼티(property)와 메소드(method)로 접근할 수 있다.

전역 객체는 ECMAScript가 실행되는 환경에 따라 다르다. 웹브라우저에서는 window 객체가 전역 객체이지만 Node.js에서는 Global 객체가 전역 객체로 존재한다.

함수 스코프(functoin scope)

함수 스코프(function scope)는 자바스크립트에서 기본 실행 단위이다. 전역 스코프(global scope)는 웹 문서가 실행되는 시점에 곧 바로 해석되어 처리된다. 반면에 함수 스코프는 해석이 되어지지만 실행 시점을 정할 수 있다는 특징을 가지고 있다.

함수 스코프(function scope)는 전역 스코프(global scope)와 비교할 때 지역 스코프(local scope)로 분리된다. 전역 스코프(global scope)에서 선언된 변수는 지역 스코프(local scope)에서 접근이 가능하지만 지역 스코프(local scope)에서 선언된 변수는 선언된 지역에 따라 접근 여부가 정해진다. 자바스크립트는 중첩 구조의 함수 형태를 가질 수 있다. 즉 함수 안에 또 다른 함수가 존재한다고 생각하면 된다. 이러한 계층 구조는 설계에 따라 2단계 이상의 계층 구조를 가질 수 있다. 변수가 선언된 함수가 어떤 위치냐에 따라서 변수의 사용 범위는 정해진다. 상위 스코프에 선언된 변수는 하위 스코프에서 접근이 가능하지만 그 반대의 경우와 동등한 레벨의 함수 스코프내의 선언된 변수는 접근이 불가능하다.

블록 스코프(block scope)

ES6에서는 let 키워드가 추가되면서 블록 단위의 유효 범위를 갖는 변수를 선언할 수 있다. 블록 스코프는 반복 처리 로직에서 변수 값을 기억해야 하는 클로저(closure) 패턴에서 매우 유용하다. 블록 스코프(block scope)는 메모리 사용에도 매우 도움이 된다. 즉  의도하든 그렇지 않든 더 이상 사용되지 않는 변수가 간혹 클로저(closure) 패턴에 묶여 제거되지 못하는 경우가 있다. 이 문제는 변수의 사용 범위가 넓은 경우에 발생할 수 있다. 블록 스코프(block scope)를 사용하므로 변수 사용 범위를 최소화하면 최적화에도 매우 많은 도움이 된다.

window.addEventListener("load", function() {  
  function process(param) {
    console.log(param);
  }
  
  // 다음 블록 스코프는 실행후 메모리에서 제거
  {
    let myVal = "Hi";
    process(myVal);
  }

  var elBtn = document.getElementById("mybutton");
  // onclick 함수로 인해 선언된 위치에서의 렉시컬 스코프가 유지
  elBtn.addEventListener("click", function onclick(e) {
    console.log("clicked");
  });
});

렉시컬 스코프(lexical scope)

자바스크립트의 처리 절차는 생각보다 복잡하다. 자바스크립트를 인터프리터(interpreter)로 처리된다고 알고 있지만 사실은 그렇지 않다. 실제로는 실행 전에 작성된 코드를 실행 코드로 변환시켜주는 절차가 있다. 실행 코드를 만드는 시점을 렉싱타임(lexing time)이라고 한다. 이때 컴파일러는 여러가지 처리를 한다. 작성된 코드의 구문을 쪼개어 의미를 만들고 스코프(scope)와 식별할 수 있는 확인자를 생성하여 실행시 사용될 수 있도록 컬렉션 구조로 만들어 준다. 이때 개발자가 코드를 작성한 것을 기준으로 스코프(scope)가 정해지게 된다. 개발자가 코드를 작성할 때 정해놓은 스코프(scope)를 컴파일 과정에서 확정한다. 이것을 렉시컬 스코프(lexical scope)라고 한다.

동적 스코프(dynamic scope)

동적 스코프(dynamic scope)는 코드가 실행되는 시점의 스코프(scope)를 의미한다. 결론부터 말한다면 동적 스코프(dynamic scope)는 사용되지 않고 렉시컬 스코프(lexical scope)만 사용된다. 다만 중요한 차이를 말한다면 렉시컬 스코프(lexical scope)는 선언된 위치, 동적 스코프(dynamic scope)는 호출된 위치와 관련이 있다. 변수 사용 범위에 대한 확정은 동적 스코프(dynamic scope)와 관계가 없다. 동적 스코프(dynamic scope)는 this 지시자와 연관성이 높다. 호출된 위치에 따라 this 지시자가 참조하는 대상이 달라진다.

컨텍스트(context)

컨텍스트(context)와 스코프(scope)를 명확히 구분하여 이해하기가 어려울 수 있다. 이것에 대한 개발자들의 의견들도 방향은 비슷하지만 견해는 다양하다. 하지만 컨텍스트(context)란 용어도 스코프(scope)만큼은 아니지만 분명히 사용되고 있다. 그리고 분명 차이는 있지만 결국은 범위를 논하는 것이다. 그러다보니 독립적인 의미에 따라 사용되기도 하고 스코프(scope)와 같은 맥락으로 사용되기도 한다. 아무튼 동일한 것은 결코 아니므로 차이에 대해 명확히 알고 사용하는 것이 적절하겠다.

이미 앞에서 설명했지만 스코프(scope)는 변수의 사용과 함수의 사용에 대한 유효 범위의 개념이라고 요약할 수 있다. 이에 비해 컨텍스트(context)는 좀더 포괄적이다. 다음은 콘텍스트(context)가 가지고 있는 환경적인 특성이다.

  • 렉시컬 스코프(lexical scope) 규칙 적용
  • 함수가 실행되어 속한 객체의 존재
  • this 지시자로 바인딩 가능

여기서 중요한 것은 this라는 존재이다. 당연히 this는 인스턴스화된 어떤 객체를 참조할 수 있는 지시자이며 일반적으로 속해 있는 함수와 연관시킨다. this는 인스턴스화된 객체를 가리키므로 함수가 실행되어야 한다. 함수가 실행되어야 한다는 분명한 사실은 컨텍스트(context)가 실행 시점에 생성된다는 의미이다. 그래서 보통 실행 컨텍스트(execution context)라 부르기도 한다.

그러나 이렇게 단순한 사실만을 가지고 있지 않은 게 문제이다. 함수와 함수는 서로 복잡하게 연결되어 있을 수 있는 것이고 함수를 호출하는 방식도 다양할 것이다. 그래서 this의 대상은 속해 있는 함수만을 전제하기는 어렵다. 함수는 호출되어야 실행되므로 결국은 호출 방식이 실행 컨텍스트(execution context)를 결정한다고 볼 수 있다.

전역 컨텍스트(global context)

전역 컨텍스트(global context)는 전역 스코프(global scope)를 가지고 있고 웹브라우저 실행 환경에서는 window 객체와 연결되어 있다. 이때 this 지시자는 window 객체를 의미한다.

함수 컨텍스트(function context)

전역 스코프(global scope)에서 함수가 선언되어 실행되면 하나의 함수 컨텍스트(function context)가 생성된다. this 지시자는 생성자 함수의 경우에는 인스턴스를 그렇지 않은 경우에는 호출된 위치에 속한 객체에 바인딩된다.

function MyFunc() {
  this.myFuncVal = "Hi";  
}

function Caller() {
  this.callerVal = ", Javascript";
  // MyFunc 함수의 myFuncVal 사용 가능
  console.log(this.myFuncVal + this.callerVal);
}

Caller.call(new MyFunc());