수학에서 여러 연산들이 복합적으로 사용될 때 연산자들 간에 우선 순위가 있는 것처럼 자바스크립트의 다양한 연산자들 사이에도 우선순위가 존재합니다. 이 우선 순위에 따라 연산자들이 평가되는 순서가 달라지게 됩니다. 사칙연산과 같은 경우는 수학과 일치하지만 다음과 같은 코드들의 결과를 예측하기는 쉽지 않습니다.
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 + 2
→ 3 * 4
→ 5 + 12
의 순서로 계산되어 17의 결과값이 나오게 됩니다.
이때 동일한 우선순위를 가진 연산자들끼리는 결합성에 따라 좌측에서 우측으로, 혹은 우측에서 좌측으로 계산됩니다. 연산자가 포함된 평가식을 왼쪽에서 오른쪽 순서로 계산한다면 좌결합성, 반대로 오른쪽부터 왼쪽으로 계산한다면 우결합성을 가진다고 합니다. 대부분의 연산자는 왼쪽에서 오른쪽 순서로 계산된다는 좌결합성을 가집니다.
foo.x = foo = 2;
할당 연산자(=
)는 우결합성을 가지기 때문에 bar
에 0을 대입한 뒤 foo
에 bar = 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
와 같이 평가됩니다. 만약 a
가 false
라면 후자의 경우 a
만 평가하고 전체 연산이 종료되지만 전자의 경우는 b || c
를 평가한 후에야 a
를 평가합니다. 쓸모없는 계산이 하나 더 많은 것입니다. 반면 a
가 true
라면 두 경우 모두 b
와 c
를 평가해야 합니다.
그리고 다음과 같은 식의 경우를 보겠습니다.
a || b && c
만약 OR가 AND보다 우선순위가 높다면(a || b) && c
와 같이 평가됩니다. 반대의 경우는 a || (b && c)가 됩니다. 전자의 경우 a
와 b
모두가 false
인 경우에만 단축 평가가 종료됩니다. 반면 후자의 경우에는 a
만 true
여도 단축평가가 종료됩니다. 그리고 a
가 false
인 경우, 전자는 b
가 false
면 평가가 종료됩니다. 그리고 b
가 true
면 끝까지 검사를 하게 됩니다. 반면 후자의 경우는 b
의 값에 상관없이 끝까지 검사를 하게 됩니다. 전체 경우의 수에서 OR이 우선인 경우 평균 2.333...인 반면, AND가 우선인 경우 2번의 계산이 필요합니다.
예시 문제 풀어보기
foo
와 bar
에는 각각 어떤 값이 할당되는가?
let foo = {n: 1};
let bar = foo;
foo.x = foo = {n: 2};
먼저 foo
는 {n: 1}
이라는 객체가 저장된 곳의 주소가 할당됩니다. 다음으로 bar
에 foo
를 할당한 상황입니다.
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.x
에 foo
가 할당되면서 모든 식의 평가가 끝이 납니다. 최종적인 결과는 다음과 같습니다.
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 이산수학 논리 연산
위키백과, 연산의 우선순위
'개발 > JavaScript' 카테고리의 다른 글
자바스크립트 await는 이벤트 루프 내에서 어떻게 동작할까 (0) | 2024.10.23 |
---|---|
동적 배열을 사용하는 자바스크립트에서 일어나는 일 (0) | 2024.10.15 |
태스크큐를 중점으로 자바스크립트 코드의 실행 순서를 알아보자 (0) | 2024.09.25 |
자바스크립트 제너레이터(Generator)로 반복가능한 객체 이터레이터(iterator)를 만들자(feat. 파이썬의 range 구현하기) (0) | 2024.07.28 |
JSDoc에서 제네릭과 타입단언 사용하기 (0) | 2024.07.21 |