ECMAScript 명세 이해, 1부


원문 : Marja Hölttä, Understanding the ECMAScript spec, part 1

이 글에서는 명세 내 간단한 함수를 이용하여 ECMAScript의 표기법을 알아본다.

서론

JavaScript에 익숙하더라도 JavaScript 언어의 명세(ECMAScript Language specification 또는 줄여서 ECMAScript spec)을 읽는 것은 상당히 어려울 수 있다. 적어도 필자가 명세를 처음 읽었을 때는 그렇게 느꼈다.

구체적인 예를 통해 명세를 이해해 보자.

다음 코드는 Object.prototype.hasOwnProperty 의 사용 예시이다.

const o = { foo: 1 };
o.hasOwnProperty('foo'); // true
o.hasOwnProperty('bar'); // false

예제에서 o 에는 hasOwnProperty 속성을 정의하지 않았으므로 프로토타입 체인을 따라 해당 프로퍼티를 찾는다. o 의 프로토타입인 Object.prototype 에서 해당 프로퍼티를 찾을 수 있다.

Object.prototype.hasOwnProperty 의 작동 방식을 설명하기 위해 명세에서는 의사코드를 사용한다.

Object.prototype.hasOwnProperty(V)

hasOwnProperty 메서드가 인수 V 와 함께 호출되었을 때, 다음 동작이 수행된다.

  1. P? ToPropertyKey(V) 로 한다.
  2. O? ToObject(this value) 로 한다.
  3. ? HasOwnProperty(O, P) 를 반환한다.

∙∙∙그리고∙∙∙

HasOwnProperty(O, P)

추상 연산 HasOwnProperty는 객체에 지정된 속성 키가 있는지 여부를 확인하여 Boolean 값을 반환한다. HasOwnProperty는 인수 OP를 사용하여 호출되며, 여기서 O는 객체이고 P는 속성 키이다. 이 추상 연산은 다음 동작을 수행한다.

  1. 단언:Type(O)Object이다.
  2. 단언:IsPropertyKey(P)true이다.
  3. desc? O.[[GetOwnProperty]](P)로 한다.
  4. descundefined이면, false가 반환된다.
  5. true가 반환된다.

그런데 추상 연산이 대체 무엇일까? [[ ]]로 묶인 것은 무엇을 의미할까? 왜 함수 앞에 ?이 있을까? 또단언(Assert)은 무엇을 의미할까?

지금부터 알아보자.

언어 타입 및 명세 타입

익숙한 것부터 시작하자. 명세는 우리가 JavaScript를 통해 이미 알고 있는 undefined, truefalse와 같은 값을 사용합니다. 이것들은 모두 언어 값이며 명세에서도 정의한 언어 타입이다.

또한 명세는 내부적으로 언어 값을 사용한다. 예를 들어 내부 데이터 타입에 값이 true 또는 false인 필드가 있을 수 있다. 그에 반해 JavaScript 엔진은 보통 내부적으로 언어 값을 사용하지 않는다. 예를 들어, C++로 작성된 JavaScript 엔진은 보통 C++ truefalse를 사용한다(JavaScript truefalse 명세를 구현한 것과는 다르다).

언어 타입 외에도 명세 타입도 사용한다. 이 타입은 JavaScript 언어에서는 사용하지 않고 명세에서만 사용하는 타입이다. JavaScript 엔진은 이 명세 타입을 (구현할 수 있지만) 구현할 필요가 없다. 이 글을 통해 명세 타입 레코드(및 subtype Completion Record)에 대해 알아보자.

추상 연산(Abstract Operations)

추상 연산은 ECMAScript 명세에 정의된 함수들을 말하며, 명세를 간결하게 작성할 목적으로 정의되었다. JavaScript 엔진은 이 추상 연산을 엔진 내부에서 별도의 기능으로 구현할 필요가 없으며 애초에 JavaScript에서 직접 호출할 수도 없다.

내부 슬롯 및 내부 메서드

내부 슬롯 및 내부 메서드[[ ]]로 묶인 이름을 사용한다.

내부 슬롯은 JavaScript 객체 또는 명세 타입의 데이터 멤버이며, 객체의 상태를 저장하는 데 사용된다. 내부 메서드는 JavaScript 객체의 멤버 함수이다.

예를 들어, 모든 JavaScript 객체에는 내부 슬롯 [[Prototype]]과 내부 메서드 [[GetOwnProperty]]가 있다.

내부 슬롯 및 메서드는 JavaScript에서 접근할 수 없다. 예를 들어 o.[[Prototype]]에 접근하거나 o.[[GetOwnProperty]]() 를 호출할 수 없다. JavaScript 엔진은 내부 사용을 위해 이를 구현할 수 있지만 꼭 그렇게 할 필요는 없다.

내부 메서드가 비슷한 이름의 추상 연산에 동작을 위임하는 경우도 있다. 일반 객체의 [[GetOwnProperty]]를 살펴보자.

[[GetOwnProperty]](P)
속성 키 P를 사용하여 O[[GetOwnProperty]] 내부 메서드를 호출하면 다음 동작이 수행된다.

  1. ! OrdinaryGetOwnProperty(O, P)가 반환된다.

(느낌표의 의미는 다음 장에서 알아볼 것이다.)

OrdinaryGetOwnProperty는 어떤 객체에도 연결되지 않았기 때문에 내부 메서드가 아니다. 대신 동작을 수행하는 객체를 매개변수로 전달한다.

OrdinaryGetOwnProperty는 일반 객체에서 동작하므로 ordinary라는 접두사를 붙인다. ECMAScript 객체는 일반 객체이거나 특수(exotic) 객체일 수 있다. 일반 객체는 필수 내부 메서드라는 메서드 집합에 대한 기본 동작을 가져야 한다. 객체가 기본 동작에서 벗어나면 특수 객체이다.

가장 잘 알려진 특수 객체는 Array인데, 길이(length) 속성이 다른 방식으로 동작하기 때문이다. 길이 속성에 값을 할당하면 Array에서 요소가 제거되는 것을 확인할 수 있다.

주요 내부 메서드는 여기에서 확인할 수 있다.

Completion Records

?!가 의미하는 것은 무엇일까? 질문의 답을 찾으려면 Completion Records을 살펴보자!

Completion Record는 명세 타입이다(명세 목적으로만 정의되었다). JavaScript 엔진은 해당 내부 데이터 타입을 구현할 필요가 없다.

Completion Record는 지정된 필드 집합을 가진 record 이다.

Completion Record에는 세 가지 필드가 있다.

Name Description
[[Type]] normal, break, continue, return, 혹은 throw 중에 하나, normal 을 제외한 모든 다른 타입들은 abrupt completion 이다.
[[Value]] completion이 발생했을 때 생성된 값이다. 예를 들어 함수의 반환 값 또는 exception(하나가 throw된 경우)이다.
[[Target]] 지시된 제어 전송에 사용된다.(이 글과 관련은 없다)

대부분의 추상 연산은 암묵적으로 Completion Record를 반환한다. 추상 연산이 Boolean과 같은 단순한 유형을 반환하는 것처럼 보이는 경우에도 암묵적으로 normal 타입의 Completion Record 로 감싼다. (Implicit Completion Values 보기)

Note 1: 명세의 일부는 동작이 위와 다르다. Completion Record에서 값을 추출하지 않고 기본 값을 반환하며, 반환 값을 그대로 사용하는 일부 helper 함수도 있다. 이 경우 명세에서는 이를 분명히 표시한다.

Note 2: 명세 편집자는 Completion Record 처리를 좀 더 명확히 보여주려 한다.

알고리즘이 예외를 발생시킨다고 가정해보자. 이는 [[Value]]가 예외 객체인 [[Type]] throw 와 함께 Completion Record 를 반환함을 의미한다. 일단 breakcontinue, return 타입은 무시한다.

ReturnIfAbrupt(argument)는 다음 동작을 수행한다.

  1. argument 가 abrupt 라면, argument 반환한다.
  2. argument 를 argument.[[Value]] 로 설정한다.

즉, Completion Record를 검사한다. abrupt completion 인 경우 즉시 반환한다. 그렇지 않으면 Completion Record에서 값을 추출한다.

ReturnIfAbrupt는 함수 호출처럼 보이지만 그렇지 않다. 이는 ReturnIfAbrupt 함수 자체가 아니라 ReturnIfAbrupt()가 발생한 함수를 반환하도록 한다. C와 같은 언어의 매크로와 더 비슷하다.

ReturnIfAbrupt는 다음과 같이 사용할 수 있다.

  1. obj 를 Foo() 로 한다. (obj 는 Completion Record.)
  2. ReturnIfAbrupt(obj)
  3. Bar(obj). (함수가 이 단락에 도달했다면, obj 는 Completion Record 에서 추출한 값이다.)

이제 물음표가 나타난다. ? Foo()ReturnIfAbrrupt(Foo())와 동일하다.

단축표기법 사용은 실용적이다. 매번 명시적으로 오류 처리 코드를 작성할 필요가 없기 때문이다.

마찬가지로, val를 ! Foo() 로 한다는 다음과 동일하다.

  1. val 를 Foo()로 한다.
  2. 단언: val 는 abrupt completion가 아니다.
  3. val 를 val.[[Value]]로 설정한다.

지금까지 배운 것을 바탕으로 Object.prototype.hasOwnProperty를 다시 작성해보면 다음과 같다.

Object.prototype.hasOwnProperty(V)

  1. P 를 ToPropertyKey(V)로 한다.
  2. P 가 abrupt completion라면, P 를 반환한다.
  3. P 를 P.[[Value]] 로 설정한다.
  4. O 를 ToObject(this value)로 한다.
  5. O 가 abrupt completion라면, O 를 반환한다.
  6. O 를 O.[[Value]] 로 설정한다.
  7. temp 를 HasOwnProperty(O, P)로 한다.
  8. temp 가 abrupt completion라면, temp 를 반환한다.
  9. temp 를 temp.[[Value]] 로 설정한다.
  10. NormalCompletion(temp) 를 반환한다.

∙∙∙그리고 HasOwnProperty를 다시 작성해보면 다음과 같다.

HasOwnProperty(O, P)

  1. 단언: Type(O) 는 Object이다.
  2. 단언: IsPropertyKey(P) 는 true이다.
  3. desc 를 O.[[GetOwnProperty]](P)로 한다.
  4. desc 가 abrupt completion라면, desc 를 반환한다.
  5. desc 를 desc.[[Value]] 로 설정한다.
  6. desc 가 undefined라면, NormalCompletion(false)를 반환한다.
  7. NormalCompletion(true)를 반환한다.

느낌표 없이 [[GetOwnProperty]] 내부 메서드를 다시 작성할 수도 있다.

O.[[GetOwnProperty]]

  1. temp 를 OrdinaryGetOwnProperty(O, P)로 한다.
  2. 단언: temp 는 abrupt completion 가 아니다.
  3. temp 를 temp.[[Value]]로 한다.
  4. NormalCompletion(temp)를 반환한다.

여기서 temp 는 다른 것과 충돌하지 않는 새로운 임시 변수라고 가정한다.

예시를 살펴보면 반환문이 Completion Record가 아닌 다른 것을 반환할 때 값을 암묵적으로 NormalCompletion로 감싼다는 것도 알 수 있다.

번외: Return ? Foo()

명세는 Return ? Foo() 표기법을 사용한다. 물음표는 왜 사용할까?

Return ? Foo()는 다음과 같이 동작한다.

  1. temp 를 Foo()로 한다
  2. temp 가 abrupt completion라면, temp를 반환한다.
  3. temp 를 temp.[[Value]]로 설정한다.
  4. NormalCompletion(temp)를 반환한다.

이는 Return Foo()와 동일하다. abrupt completions 와 normal completions 모두에 대해 동일한 방식으로 동작한다.

Return ? Foo()는 편집상의 이유로만 사용되어 Foo가 Completion Record를 반환한다는 것을 더 명확하게 만든다.

단언(Assert)

명세에서의 단언은 알고리즘의 불변 조건을 의미한다. 이러한 정보는 명확성을 위해 추가된 것으로, 구현 시 요구 사항이 추가되지는 않는다. 따라서 구현 시 확인할 필요가 없다.

덧붙이기

추상 연산은 다른 추상 연산(아래 그림 참조)으로 위임되지만, 이 글을 기반으로 해당 작업이 무엇인지 파악할 수 있다. 다른 명세 타입인 속성 설명자가 표시된다.

Understanding_ECMAScript_part_1

요약

우리는 간단한 메서드인 Object.protype.hasOwnProperty와 이 메서드가 호출하는 추상 연산에 대해 알아보았다. 오류 처리와 관련된 단축표기법 ?!에도 익숙해졌다. 그리고 언어 타입, 명세 타입, 내부 슬롯 및 내부 메서드를 알아보았다.

유용한 링크

How To Read the ECMA Specification: 이 글에서 다루고 있는 대부분의 내용을 약간 다른 각도로 바라보고 작성된 튜토리얼이다.

김정용2022.11.16
Back to list