수학에서 여러 연산들이 복합적으로 사용될 때 연산자들 간에 우선 순위가 있는 것처럼 자바스크립트의 다양한 연산자들 사이에도 우선순위가 존재합니다. 이 우선 순위에 따라 연산자들이 평가되는 순서가 달라지게 됩니다. 사칙연산과 같은 경우는 수학과 일치하지만 다음과 같은 코드들의 결과를 예측하기는 쉽지 않습니다.

let foo = {n: 1};
let bar = foo;
foo.x = foo = {n: 2};
// foo와 bar의 값은?
let i = 3;
console.log(i++ + ++i * 2);
// 출력 값은?
let a = 10;
let b = 20;
a += b -= 5;
// a, b 각각의 값은?
let p = 5;  // 0101 (2진수)
let q = 3;  // 0011 (2진수)
console.log(p & q | p ^ q);

연산자의 우선 순위란?

연산자 우선순위는 여러 연산자가 사용된 식에서 어떤 연산자가 먼저 수행될지를 결정하는 규칙입니다. 높은 우선순위를 가진 연산자는 다른 연산자보다 먼저 평가됩니다. 우선순위는 1부터 19까지의 가중치를 가지며 가중치가 높을 수록 먼저 평가됩니다.

console.log(5 + (1 + 2) * 4);

괄호를 통한 그룹이 최우선 순위를 가지고 수학과 같이 곱하기가 더하기보다 더 높은 우선순위를 가지므로 1 + 23 * 45 + 12의 순서로 계산되어 17의 결과값이 나오게 됩니다.

이때 동일한 우선순위를 가진 연산자들끼리는 결합성에 따라 좌측에서 우측으로, 혹은 우측에서 좌측으로 계산됩니다. 연산자가 포함된 평가식을 왼쪽에서 오른쪽 순서로 계산한다면 좌결합성, 반대로 오른쪽부터 왼쪽으로 계산한다면 우결합성을 가진다고 합니다. 대부분의 연산자는 왼쪽에서 오른쪽 순서로 계산된다는 좌결합성을 가집니다.

foo.x = foo = 2;

할당 연산자(=)는 우결합성을 가지기 때문에 bar에 0을 대입한 뒤 foobar = 0의 반환값인 0을 대입합니다. (대입 연산자는 대입된 값을 반환합니다.)

연산자 우선 순위

다음은 각 연산자 우선순위와 그에 따른 가중치입니다.

   
가중치 연산자
19 그룹 ()
18 멤버(프로퍼티, 메서드) 접근 . []
new
함수 호출
옵셔널 체이닝 ?.
17 인자가 없는 new
16 후위증감 연산자 number++, number--
15 논리 not !
비트 not ~
단항 부호 +number -number
전위 증감 연산자 ++number --number
typeof, void, delete, await
14 거듭제곱 **
13 곱하기 *, 나누기 /, 나머지 %
12 더하기 +, 빼기 -
11 비트 연산 << >> >>>
10 미만 <, 이하 <=, 초과 >, 이상 >=
in
instanceof
9 동등비교 연산자 ==, 부등 비교 연산자 !=, 일치 연산자 ===, 불일치 연산자 !==
7 비트 AND &, 비트 XOR
6 비트 OR
5 논리 AND &&
4 논리 OR
3 삼항 조건 연산자 조건 ? true : false
2 할당 연산자 = += -=를 포함한 모든 할당 연산자
yield, yield*
1 쉼표 ,

논리와 수학: AND 연산자가 OR 연산자보다 우선 순위가 높은 이유

이번에 우선 순위를 공부하면서 AND 연산자가 OR 연산자보다 우선 순위가 높은 이유에 대해 흥미로운 사실을 발견했습니다. 우선 순위가 이렇게 정해진 이유를 논리성과 효율성에 관점에서 살펴보겠습니다.

논리성

효율성을 따지기 전에 논리적으로 정확한 지에 대해 먼저 따져보겠습니다. 먼저 수학에서 AND는 두 조건이 모두 참일 때 참이 되는 논리곱, OR은 두 조건 중 하나라도 참일 경우 참이 되는 논리합으로 나타냅니다. 논리 연산에서도 논리곱이 논리합보다 우선하는 이유는 강력한 조건을 먼저 평가함으로써 논리적 일관성과 명확성을 유지하기 위함입니다. 물론 논리적 일관성과 명확성을 유지하기 위해서 OR을 먼저 평가하도록 정할 수도 있다고 생각할 수도 있습니다. 결국 "정하기 나름 아닌가?"라는 생각이 들것입니다. 하지만 효율성에 대해 생각한다면 AND 조건을 우선시하는 것이 더 나은 선택이라는 것을 알게 될 것입니다.

효율성

효율성에 관한 부분은 저의 추론에 관한 내용으로 사실과 다를 수 있습니다.

 

효율성을 알아보기 이전에 단축 평가에 대해 먼저 알아보겠습니다. 단축 평가는 AND 또는 OR 연산의 효율을 위해 도입되었는데요. AND 연산의 경우 좌측부터 순서대로 평가하면서 처음 flase로 평가되는 값을 만나는 순간 해당 값을 반환하면서 연산을 종료합니다. 끝까지 false로 평가되는 값을 발견하지 못하면 마지막 값을 반대로 OR 연산은 처음 true로 평가되는 값을 만나면 해당 값을 반환하면서 연산을 종료합니다. 끝까지 true를 발견하지 못하면 마지막 값을 반환하면서 연산을 종료합니다.

 

자 그럼, 다음의 표현식을 평가한다고 생각해 보겠습니다.

a && b || c

만약 OR가 AND보다 우선순위가 높다면 a && (b || c)와 같이 평가됩니다. 이렇게 되면 b || c의 결과를 먼저 알아야 전체 식을 계산할 수 있습니다. 그리고 AND가 OR보다 우선순위가 높다면 (a && b) || c와 같이 평가됩니다. 만약 afalse라면 후자의 경우 a만 평가하고 전체 연산이 종료되지만 전자의 경우는 b || c를 평가한 후에야 a를 평가합니다. 쓸모없는 계산이 하나 더 많은 것입니다. 반면 atrue라면 두 경우 모두 bc를 평가해야 합니다.

 

그리고 다음과 같은 식의 경우를 보겠습니다.

a || b && c

만약 OR가 AND보다 우선순위가 높다면(a || b) && c와 같이 평가됩니다. 반대의 경우는 a || (b && c)가 됩니다. 전자의 경우 ab모두가 false인 경우에만 단축 평가가 종료됩니다. 반면 후자의 경우에는 atrue여도 단축평가가 종료됩니다. 그리고 afalse인 경우, 전자는 bfalse면 평가가 종료됩니다. 그리고 btrue면 끝까지 검사를 하게 됩니다. 반면 후자의 경우는 b의 값에 상관없이 끝까지 검사를 하게 됩니다. 전체 경우의 수에서 OR이 우선인 경우 평균 2.333...인 반면, AND가 우선인 경우 2번의 계산이 필요합니다.

예시 문제 풀어보기

foobar에는 각각 어떤 값이 할당되는가?

let foo = {n: 1};
let bar = foo;
foo.x = foo = {n: 2};

먼저 foo{n: 1}이라는 객체가 저장된 곳의 주소가 할당됩니다. 다음으로 barfoo를 할당한 상황입니다.

foo, bar → { n: 1 }

다음 줄에서 .을 통한 프로퍼티 접근 연산의 우선 순위는 할당 연산자보다 높습니다. 따라서 foo.x는 먼저 평가가 이루어집니다. 그래서 현재의 foo{n:1}이라는 객체의 x프로퍼티에 대해 연산합니다.

foo, bar → { n: 1 }
foo.x는 {n: 1} 객체의 x 프로퍼티를 바라본다.

이후 할당 연산자(=)를 만나는데, 해당 연산자는 우결합성을 가지기 때문에 다음 피연산자의 평가가 먼저 이루어져야 합니다. 여기서 다음 연산자 역시 할당 연산자로 우측의 피연산자를 먼저 평가하려고 합니다. 따라서 foo{n: 2}라는 리터럴이 먼저 할당되어 foo에는 {n: 2}가 할당됩니다.

foo → {n:2}
bar → {n:1}
foo.x는 앞에서 이미 평가 되었으므로 여전히 bar가 바라보고 있는 {n: 1} 객체의 x 프로퍼티를 바라본다.

이후 미리 평가되었던 foo.xfoo가 할당되면서 모든 식의 평가가 끝이 납니다. 최종적인 결과는 다음과 같습니다.

foo → {n:2}
bar → {n:1, x: foo}

이는 다음 코드를 실행시켜보면 쉽게 알 수 있습니다.

foo.n = 3;
console.log(bar); // {n: 1, x: {n: 3}}

다음 코드의 출력 값은?

let i = 3;
console.log(i++ + ++i * 2);

먼저 우선순위가 높은 i++ 가 평가됩니다. 따라서 i 는 4로 증가합니다. 하지만 후위 증가 연산자는 증가하기 전 원래의 값을 반환하므로 평가된 값은 3입니다.

 

3 + ++i * 2

 

그리고 다음으로 우선순위가 높은 ++i가 평가되는데, 이로 인해 i 는 5로 증가합니다. 전위 증가 연산자는 증가한 후의 값을 반환하기 때문에 5를 반환합니다.

3 + 5 * 2

 

여기서는 수학의 사칙 연산과 우선 순위가 동일하므로 결과는 13이 됩니다.

a, b 각각의 값은?

let a = 10;
let b = 20;
a += b -= 5;

두 개의 연산자 모두 += 동일하므로 할당 연산자의 결합성에 따라 우측 연산자가 먼저 평가됩니다. 따라서 b 에서는 5를 빼고 a 에 그 결과 값인 15를 더하게 됩니다. 따라서 결과는 a는 25, b 는 15가 됩니다.

여러 연산자를 사용할 때는 괄호를 이용하자

사실 모든 연산자의 우선순위를 외울 수는 없습니다. 사칙 연산이나 괄호와 같은 연산자는 수학과 비슷한 상대적인 우선 순위 관계를 같지만 그 외에도 수많은 연산자가 존재합니다. 그렇기에 연습 문제와 같이 우선순위를 명확히 알아볼 수 없는 코드를 작성하는 것을 지양하고 괄호()를 이용해 각 연산의 우선순위를 명확히 하거나 한 번에 적은 수의 연산자를 이용하여 연산 내용을 쉽게 파악할 수 있도록 하는 것이 좋습니다.

참고 자료

MDN, 연산자 우선순위

동국대학교, Jinseog Kim 이산수학 논리 연산

위키백과, 연산의 우선순위

+ Recent posts