자바스크립트의 함수 (2) - 함수 호출


저번 글을 통해 함수가 어떻게 생성되는지 대략적으로 살펴보았다.

  • 함수는 일반 객체의 확장이다.
  • 함수 생성 시점에 그 함수의 역할이 어느정도 결정될 수 있다.(callable과 constructor)
  • 함수 생성 시점에 저장하는 데이터들을 통해 스코프나 this참조 방식을 결정한다.

이제 ECMAScript가 실제로 이 함수 객체를 함수로 호출할 때 어떤식으로 동작하는지 찾아보자. 이 글에서는 제너레이터 함수나 async 함수 등 조금은 특별한 함수보다는 일반적인 함수와 일반적인 호출에 대해서만 알아보려 한다.

함수 호출 - Call(F, V, [, argumentsList])

ECMAScript 2017은 함수 호출을 Call(F, V, [, argumentsList])으로 표현한다. Call은 함수 객체의 내부 [[Call]] 메서드를 수행하는 동작(Abstract operation)으로 F, V, argumentsList를 인자로 받는다. F는 함수 객체, V[[Call]]this 값, argumentsList는 함수 호출 시 전달한 인자로 기본값은 빈 리스트이다.

  1. argumentsList가 전달되지 않았으면 빈 리스트로 지정한다.
  2. FCallable이 아니면 에러를 발생시킨다.
  3. F.[[Call]](V, argumentsList) 수행 결과를 반환한다.

3번에서 알 수 있듯 실질적인 함수의 호출과 그 동작은 F.[[Call]]에 나타나 있다. 이제 F.[[Call]]을 알아보자.

참고로 ECMAScript에서 함수를 호출하는 동작을 위와 같이 표현한 것일 뿐, 실제 자바스크립트 엔진이 ECMAScript2017의 Call이라는 연산을 위와 똑같이 정의한 것은 아니다.

함수 객체의 [[Call]] - F.[[Call]](thisArgument, argumentsList)

지난 글에서 설명했듯 [[Call]]은 함수 객체의 내부 메서드(Internal method)다. 인자로 this값과 argumentsList를 받는다.

  1. F.[[FunctionKind]]가 "classConstructor"라면 에러를 발생시킨다.
  2. callerContext는 현재 실행 중인 실행 컨텍스트(running execution context)
  3. calleeContext에 새로운 실행 컨텍스트를 생성하여 지정한다. (PrepareForOrdinaryCall)
  4. (-- assertion: 새로 만들어진 calleeContext가 현재 running execution context이다.)
  5. this를 바인딩한다. (OrdinaryCallBindThis)
  6. 함수 코드를 수행하고 result에 그 결과를 저장한다. (OrdinaryCallEvaluateBody)
  7. calleeContext를 execution context stack에서 제거하고, callerContext를 다시 running execution context로 지정한다.
  8. result를 반환한다.

위 동작에서 Execution Context, execution context stack, running exeuction context 내용이 조금 난해할 수 있을 것 같다. 우선은 이런 게 있다 정도로만 생각하고, 앞으로 차근차근 알아가 보도록 하자.

함수 호출을 조금 더 쉽게 정리해보면 다음과 같다.

  1. 함수를 호출하면 이에 맞춰서 함수를 실행할 수 있는 환경을 만들고 초기화한다.
  2. this를 바인딩한다. (this가 어떤 객체를 참조해야 할지 결정한다.)
  3. 실제 함수 코드를 수행하고 그 결과를 result에 저장한다.
  4. 이 함수를 호출했던 곳(환경)으로 돌아가며,
  5. result를 반환한다.

Execution Context

Execution Context(이하 EC)는 스코프(식별자 이름과 값의 매칭)와 기본 객체들(intrinsic objects - Array, Object 등의 기본 생성자와 그 프로토타입 등)을 가지고 있는 Realm 등 코드 수행 환경에 대한 여러 정보를 가지고 있는 어떤 장치라고 생각하면 된다. 결국, EC는 ECMAScript에서 코드 수행(evaluation) 매커니즘을 표현하기 위한 것이며, 실제 스크립트 엔진들은 이 명세와 완벽히 일치하지 않을 수 있다.

자바스크립트가 단일 쓰레드 환경으로 코드를 컴파일하고 실행할 때 call stack을 만드는 것과 같이 EC Stack을 만든다고 생각해보자. 가장 밑바탕에는 Global 코드 환경에 대한 EC가 있을 것이고, 그 위로 함수가 호출될 때마다 그에 맞는 EC가 하나씩 추가되고 빠지기를 반복할 것이다. 그리고 각 시점에 stack의 최상위에 있는 EC가 바로 running execution context인 것이다.

다음과 같은 코드에서 EC stack이 어떻게 변하는지 살펴보자.

// global

function foo() {
  function bar() {
    return "bar";
  }

  return bar();
}

foo();
                      |--------|
                      | bar    |
           |--------| |--------| |--------|
           | foo    | | foo    | | foo    |
|--------| |--------| |--------| |--------| |--------|
| global | | global | | global | | global | | global |
|--------| |--------| |--------| |--------| |--------|

ECMAScript 코드 수행을 위한 EC에는 LexicalEnvironment와 VariableEnvrironment라는 컴포넌트가 존재한다. 간단히 변수의 참조를 기록하는 환경이라고 생각하면 된다. LexicalEnvironment와 VariableEnvironment가 서로 나누어져 있지만, 사실은 초기화 시에 같은 객체를 바라보고 있다. with와 같은 특별한 문장을 만나면 그 block 내부에서는 새로 만들어진 LexicalEnvironment를 참조한다.

(얼핏 살펴본 바로는 기본값 parameter도 Lexical/VariableEnvironment와 관련이 있다. 앞으로 이어질 3편 글에서 Environment, Environment Record 그리고 this에 대해 조금 더 자세히 알아볼 예정이다.)

OrdinaryCall

위에서 알아본 [[Call]]의 동작에서 OrdinaryCall 이라는 단어가 3번 등장했다.

  1. PrepareForOrdinayCall
  2. OrdinaryCallBindThis
  3. OrdinaryCallEvaluateBody

위 3개는 ECMAScript에서 정의하고 있는 내부 동작인데 하나씩 살펴볼 필요가 있을 것 같다.

PrepareForOrdinayCall

PrepareForOrdinayCall은 결국 EC를 만들고 초기화 시키는 내용을 가진 동작을 추상적으로 표현한것인데 조금 더 자세히 보면 아래 3단계를 갖는다. (Realm은 이 글에서는 큰 관련이 없어 설명에서 제외하였다.)

  1. 새로운 EC를 생성한다. (calleeContext)
  2. calleeContext 들어갈 Lexical Environment를 생성한다.
  3. calleeContext를 EC Stack에 추가(push)한다. 따라서 calleeContext가 running execution context가 된다.

OrdinayCallBindThis

OrdinayCallBindThis는 함수 객체의 [[ThisMode]]에 따른 this값 참조를 결정한다. 화살표 함수(Arrow Function), Strict mode, Environment와 연결된다.

  1. [[ThisMode]]가 lexical인 경우는 다른 처리를 하지 않는다. (arrow function)
  2. [[ThisMode]]가 strict인 경우 인자로 넘어온 thisArgument를 Environment record에 설정한다.
  3. [[ThisMode]]가 lexical도 아니고 strict도 아닌 경우 global에 있는 [[thisValue]]를 Environment record에 설정한다.

OrdinaryCallEvaluateBody

OrdinaryCallEvaluateBody는 다음 두개 동작으로 나눌 수 있다.

  1. 변수 선언 초기화 (FunctionDeclarationInstantiation)
  2. 코드 수행 및 결과 반환

변수 선언 초기화는 결국 Environment Record(식별자 이름과 값의 매칭을 위한 표 정도로 이해할 수 있을 것 같다)를 채우는 동작인데, 꽤 복잡한 동작들이 작성돼 있다. 특히 arguments 객체가 필요한지 아닌지, 기본값 매개변수가 있는지 없는지에 따라 분기가 나뉘는데 결국에는 또 EC의 LexicalEnvironment와 VariableEnvironment로 이어지게 된다.

그래서..

결국, 함수의 호출을 이해하기 위해서는 EC와 EC 내부의 LexicalEnvironment, VariableEnvironment를 이해해야 한다. 함수 한번 호출하는데 참 많은 내용을 알아야 하면서도 이렇게까지 알아야 하나 싶기도 하는 마음도 들기 시작했다. 하지만 그래도 ECMAScript의 함수를 이해하고 코드를 작성하는 것은 자유도가 높은 자바스크립트에서 예기치 않은 버그를 방지할 수 있는 데 많은 도움이 될 것 같다.

그리고 앞으로 이어질 글에서 Environment와 Record, 변수 선언 초기화 등에 대해 더 자세히 알아보려 한다. 잠시 예고를 하자면, 아래에 있는 JS 코드가 어떻게 수행되고 또 왜 에러가 발생하는지 그 이유를 알 수 있을 것이다.

function add(a, b) {
  return a + b;
}

function foo(a, b = add(a, 1)) {
  return `foo ${a + b}`;
}

function bar(a = add(b, 1), b) {
  return `bar ${a + b}`;
}

console.log(foo(1)); // foo 3
console.log(bar(undefined, 1)); // Error

Reference

이민규2017.08.11
Back to list