본문 바로가기
공뷰/js,ts

ECMAScript 연산자의 동작 - 덧셈과 곱셈 연산(+, -, *, /, %)

by WOOSERK 2023. 4. 19.

모던 자바스크립트의 암묵적 타입 변환 파트를 읽던 중에 궁금해졌다.


+

ECMA 사양에는 다음처럼 서술되어 있습니다.

+ 연산자는 문자열을 합치거나 숫자의 덧셈을 수행한다고 합니다. 그리고 연산 결과는 EvaluateString...을 반환한다고 하는데, 자세히 살펴보겠습니다.

 

lref는 왼쪽 피연산자의 평가 결과고, lval은 그것이 참조하고 있는 값을 의미합니다. r은 오른쪽이겠죠?

그리고 ApplyString...을 반환한다고 하는데, 들어가 보겠습니다.

 

길게 적혀있지만 사실 단순합니다.

우선, 8번에 적힌 테이블에 있는 연산자에 대해서만 이후 과정을 진행한다고 합니다. + 연산은 T:add라는 이름으로 포함되어 있습니다.

 

양쪽 피연산자를 primitive 타입으로 변환합니다. 이때, 한쪽이라도 String 타입이라면 양쪽을 문자열로 변환하여 합친 후 반환합니다. ToString()으로 문자열로 변환할 수 없으면 TypeError를 발생시킵니다. (ex. Symbol 타입)

 

primitive 타입으로 변환했으면 이제 숫자형으로 변환합니다. ToNumeric()으로 숫자형으로 변환할 수 없으면 TypeError를 발생시킵니다. (ex. Symbol 타입)

주의할 점은 undefined의 경우 숫자형인 NaN으로 변환됩니다.

 

만약 두 타입이 다르면 TypeError를 발생시킵니다. 

 

그 후 두 수를 더한 후 반환합니다.


-

ECMA 사양에는 다음처럼 서술되어 있습니다.

- 연산은 피연산자들의 차를 계산하는 데 사용됩니다. 당연함. 빼기 연산임.

 

+ 연산과 동일한 결과를 반환하지만 반환하는 함수의 인자가 +에서 -로 변경된 것을 확인할 수 있습니다.

 

덧셈 연산에서는 opText가 +였지만 이번에는 -입니다.

 

+ 연산이 아니므로 2번 과정은 수행하지 않아 제외했습니다. + 연산과 다른 점은 primitive 타입 변환이나 문자열 변환 과정을 거치지 않습니다. 바로 숫자형으로 변환합니다. 그 후 T::subtract 연산을 수행하고, 반환합니다.

 

*, /, %

*와 /, % 연산의 ECMA 사양은 다음과 같습니다.

*와 /, % 모두 우리가 잘 알고 있는 연산을 수행한다고 적혀있습니다. 

 

동작을 살펴보면, 반환하는 부분에서 익숙한 함수가 보입니다. 이전에 +, -와 같은 함수를 실행하고, 결과를 반환합니다. 달라진 것은 opText가 *, /, %라는 점입니다.

따라서 위 테이블에 따라 T::multiply 혹은 T::divide, T::remainder를 실행하고, 결과를 반환합니다.


+) BNF

ECMA 사양에서 AdditiveExpression과 MultiplicativeExpression 등의 용어가 있었습니다.  대충 더하는 표현식이랑 곱하는 표현식을 나타내는 것 같은데, 갑자기 곱하는 표현식이 왜 나올까요?

 

BNF

이걸 이해하려면 BNF를 알아야 하는데요. BNF는 프로그래밍 언어와 같은 문맥 자유 언어의 문법을 나타내기 위한 표현법입니다.

 

BNF를 따라 parse 트리를 만들 수 있는데, 트리가 2개 이상 만들어지면 모호한 문법이 됩니다. 모호함을 없애기 위해서는 연산자 우선 순위 결합 방향에 대해 규칙을 정의해줘야 합니다.

 

1. 연산자 우선 순위

ECMA 사양에 적힌 연산자 우선순위는 다음처럼 표현할 수 있습니다. 위에 있을수록 우선순위가 높습니다. 

이렇게 계층화를 하려면 연산자와 키워드들을 범주로 나누어야 합니다. Multiplicative에는 곱셈 연산들이, Additive에는 덧셈 연산들이 속해있습니다.

 

이게 왜 연산자 우선 순위를 나타내느냐? 5+3*4를 예시로 들어보겠습니다.

 

위 두 BNF에 따라 5+3*4는 다음과 같은 트리로 만들어집니다. 사실 다른 BNF도 봐야하지만 간소화했습니다. 이해하지 않으셔도 괜찮습니다.

 

parse 트리는 가장 아래에서부터 연산을 시작합니다. Additive는 Multiplicative로 변환할 수 있습니다. 즉, 변환하는 과정을 거침으로써 트리에서 더 아래에 위치할 수 있습니다.

 

따라서 3*4가 먼저 수행되고, 그 결과가 5와 더해집니다. 곱셈이 덧셈보다 우선순위가 높아지게 됩니다.

 

2. 결합 방향

어디서 재귀를 하느냐로 결합 방향을 나타낼 수 있습니다. 이를 잘 설명하기 위해, 곱셈으로 예를 들어보겠습니다.

MultiplicativeExpression : MultiplicativeExpression  MultiplicativeOperator  ExponentiationExpression

 

왼쪽에 MultiplicativeExpression이 다시 나타났으므로 왼쪽에서 재귀를 하고 있습니다. 따라서 결합 방향은 좌->우가 됩니다. 이게 왜 좌->우인지는 5 * 5 / 3 이라는 표현식으로 예시를 들어보겠습니다.

 

위 BNF로 parse 트리를 그리면 다음과 같은 모양으로 만들어집니다.

parse 트리는 가장 아래에서부터 연산을 한다고 했습니다. 따라서 5*5이 먼저 수행되고, 그 결과가 3과 나눠집니다. 따라서, 같은 우선순위인 *와 / 중 왼쪽의 * 연산이 먼저 수행되므로 좌->우 결합 방향을 가집니다.

 

반대로 오른쪽에서 재귀할 경우를 생각해보겠습니다. BNF는 다음처럼 표현될 것입니다.

MultiplicativeExpression : ExponentiationExpression  MultiplicativeOperator  MultiplicativeExpression  

 

parse 트리는 다음처럼 그려집니다.

아래에서부터 연산을 하므로 5/3이 먼저 수행됩니다. 따라서 우->좌 결합 방향을 가집니다.

 

자바스크립트는 모든 숫자를 실수로 다루므로 상관없지만, 정수와 실수가 나누어져 있는 Java나 C의 경우 여기서 문제가 됩니다. Java나 C는 5/3의 연산 결과가 1이 나옵니다. 따라서 5와 1이 곱해져서 최종 결과는 5가 됩니다.

 

10이 나와야하는데 5가 나왔습니다. 기본적으로 수식은 왼쪽에서 오른쪽으로 연산해야 합니다. 수학적 약속을 어긴 문법이므로, 이 언어는 폐기해야 합니다.


길어서 안 읽었습니다.

그럴 줄 알고 요약을 준비했습니다. 오늘 다룬 글의 요약은 다음과 같습니다. 

  1. +는 피연산자 중 하나라도 문자열 타입이면 문자열을 합치는 연산을 수행한다.
  2. +, -, *, /, % 모두 피연산자를 숫자로 변환한 후 연산을 수행한다.
  3. *, /, %는 +, -보다 연산 순서가 빠르다.
  4. +, -, *, /, % 모두 좌->우 결합이다.