논리연산자와 nullish 병합 연산자 ??

RHUK2·2021년 12월 22일
0

Javascript

목록 보기
7/56

📢 22/07/26 복습


📚 Reference


javascript.info, https://ko.javascript.info/logical-operators
javascript.info, https://ko.javascript.info/nullish-coalescing-operator

참고 사이트에 내용을 개인적으로 복습하기 편하도록 재구성한 글입니다.
자세한 설명은 참고 사이트를 살펴보시기 바랍니다.


논리 연산자


자바스크립트엔 세 종류의 논리 연산자 ||(OR), &&(AND), !(NOT)이 있습니다.

연산자에 "논리"라는 수식어가 붙긴 하지만 논리 연산자는 피연산자로 불린형뿐만 아니라 모든 타입의 값을 받을 수 있습니다. 연산 결과 역시 모든 타입이 될 수 있습니다.


|| (OR)


OR 연산자는 두 개의 수직선 기호로 만들 수 있습니다.

result = a || b;

전통적인 프로그래밍에서 OR 연산자는 불린값을 조작하는 데 쓰입니다. 인수 중 하나라도 true이면 true를 반환하고, 그렇지 않으면 false를 반환하죠.

자바스크립트의 OR 연산자는 다루긴 까다롭지만 강력한 기능을 제공합니다. OR을 어떻게 응용할 수 있는지 알아보기 전에 먼저, OR 연산자가 불린값을 어떻게 다루는지 알아보도록 합시다.

OR 연산자는 이항 연산자이므로 아래와 같이 네 가지 조합이 가능합니다.

alert( true || true );   // true
alert( false || true );  // true
alert( true || false );  // true
alert( false || false ); // false

피연산자가 모두 false인 경우를 제외하고 연산 결과는 항상 true입니다.

피연산자가 불린형이 아니면, 평가를 위해 불린형으로 변환됩니다.

예를 들어, 연산 과정에서 숫자 1true로, 숫자 0false로 바뀌죠.

if (1 || 0) { // if( true || false ) 와 동일하게 동작합니다.
  alert( "truthy!" );
}

첫 번째 truthy를 찾는 ||


지금까진 피연산자가 불린형인 경우만을 다뤘습니다. 전통적인 방식이죠. 이제 자바스크립트에서만 제공하는 논리연산자 OR의 "추가"기능에 대해 알아보겠습니다.

OR 연산자와 피연산자가 여러 개인 경우는 아래와 같은 알고리즘으로 동작합니다.

result = value1 || value2 || value3;

이때, OR 연산자는 다음 순서에 따라 연산을 수행합니다.

🕐 가장 왼쪽 피연산자부터 시작해 오른쪽으로 나아가며 피연산자를 평가합니다.
🕑 각 피연산자를 불린형으로 변환합니다. 변환 후 그 값이 true이면 연산을 멈추고 해당 피연산자의 변환 전 원래 값을 반환합니다.
🕒 피연산자 모두를 평가한 경우(모든 피연산자가 false로 평가되는 경우)엔 마지막 피연산자를 반환합니다.

💥 여기서 핵심은 반환 값이 형 변환을 하지 않은 원래 값이라는 것입니다.

alert( 1 || 0 ); // 1 (1은 truthy임)

alert( null || 1 ); // 1 (1은 truthy임)
alert( null || 0 || 1 ); // 1 (1은 truthy임)

alert( undefined || null || 0 ); // 0 (모두 falsy이므로, 마지막 값을 반환함)

이런 OR 연산자의 추가 기능을 이용하면 여러 용도로 OR 연산자를 활용할 수 있습니다.

단락 평가

OR 연산자가 제공하는 또 다른 기능은 "단락 평가(short circuit evaluation)"입니다.

위에서 설명해 드린 바와 같이 ||은 왼쪽부터 시작해서 오른쪽으로 평가를 진행하는데, truthy를 만나면 나머지 값들은 건드리지 않은 채 평가를 멈춥니다. 이런 프로세스를 "단락 평가"라고 합니다.

단락 평가의 동작 방식은 두 번째 피연산자가 변수 할당과 같은 부수적인 효과(side effect)를 가지는 표현식 일 때 명확히 볼 수 있습니다.

아래 예시를 실행하면 두 번째 메시지만 출력됩니다.

true || alert("not printed");
false || alert("printed");

첫 번째 줄의 || 연산자는 true를 만나자마자 평가를 멈추기 때문에 alert가 실행되지 않습니다.

단락 평가는 연산자 왼쪽 조건이 falsy일 때만 명령어를 실행하고자 할 때 자주 쓰입니다.


&& (AND)


두 개의 앰퍼샌드를 연달아 쓰면 AND 연산자 &&를 만들 수 있습니다.

result = a && b;

전통적인 프로그래밍에서 AND 연산자는 두 피연산자가 모두가 참일 때 true를 반환합니다. 그 외의 경우는 false를 반환하죠.

alert( true && true );   // true
alert( false && true );  // false
alert( true && false );  // false
alert( false && false ); // false

OR 연산자와 마찬가지로 AND 연산자의 피연산자도 타입에 제약이 없습니다.

if (1 && 0) { // 피연산자가 숫자형이지만 논리형으로 바뀌어 true && false가 됩니다.
  alert( "if 문 안에 falsy가 들어가 있으므로 alert창은 실행되지 않습니다." );
}

첫 번째 falsy를 찾는 &&


AND 연산자와 피연산자가 여러 개인 경우를 살펴봅시다.

result = value1 && value2 && value3;

AND 연산자 &&는 아래와 같은 순서로 동작합니다.

🕐 가장 왼쪽 피연산자부터 시작해 오른쪽으로 나아가며 피연산자를 평가합니다.
🕑 각 피연산자는 불린형으로 변환됩니다. 변환 후 값이 false이면 평가를 멈추고 해당 피연산자의 변환 전 원래 값을 반환합니다.
🕒 피연산자 모두가 평가되는 경우(모든 피연산자가 true로 평가되는 경우)엔 마지막 피연산자가 반환됩니다.

💥 여기서 핵심은 반환 값이 형 변환을 하지 않은 원래 값이라는 것입니다.

// 첫 번째 피연산자가 truthy이면,
// AND는 두 번째 피연산자를 반환합니다.
alert( 1 && 0 ); // 0
alert( 1 && 5 ); // 5

// 첫 번째 피연산자가 falsy이면,
// AND는 첫 번째 피연산자를 반환하고, 두 번째 피연산자는 무시합니다.
alert( null && 5 ); // null
alert( 0 && "아무거나 와도 상관없습니다." ); // 0

AND 연산자에도 피연산자 여러 개를 연속해서 전달할 수 있습니다. 첫 번째 falsy가 어떻게 반환되는지 예시를 통해 살펴봅시다.

alert( 1 && 2 && null && 3 ); // null

아래 예시에선 AND 연산자의 피연산자가 모두 truthy이기 때문에 마지막 피연산자가 반환됩니다.

alert( 1 && 2 && 3 ); // 마지막 값, 3

🔥 &&의 우선순위가 ||보다 높습니다.

AND 연산자 &&의 우선순위는 OR 연산자 ||보다 높습니다. 따라서 a && b || c && d(a && b) || (c && d)와 동일하게 동작합니다.


🔥 if||&&로 대체하지 마세요.

어떤 개발자들은 AND 연산자 &&if문을 "짧게" 줄이는 용도로 사용하곤 합니다.

let x = 1;

(x > 0) && alert( "0보다 큽니다!" );

&&의 오른쪽 피연산자는 평가가 && 우측까지 진행되어야 실행됩니다. 즉, (x > 0)이 참인 경우에만 alert문이 실행되죠.

위 코드를 if 문을 써서 바꾸면 다음과 같습니다.

let x = 1;

if (x > 0) alert( "0보다 큽니다!" );

&&를 사용한 코드가 더 짧긴 하지만 if문을 사용한 예시가 코드에서 무엇을 구현하고자 하는지 더 명백히 드러내고, 가독성도 좋습니다. 그러니 if 조건문이 필요하면 if를 사용하고 AND 연산자는 연산자 목적에 맞게 사용합시다.


! (NOT)


논리 연산자 NOT은 느낌표 !를 써서 만들 수 있습니다.

NOT 연산자의 문법은 매우 간단합니다.

result = !value;

NOT 연산자는 인수를 하나만 받고, 다음 순서대로 연산을 수행합니다.

🕐 피연산자를 불린형(true / false)으로 변환합니다.
🕑 위에서 변환된 값의 역을 반환합니다.

alert( !true ); // false
alert( !0 ); // true

NOT을 두 개 연달아 사용(!!)하면 값을 불린형으로 변환할 수 있습니다.

alert( !!"non-empty string" ); // true
alert( !!null ); // false

이때, 첫 번째 NOT 연산자는 피연산자로 받은 값을 불린형으로 변환한 후 이 값의 역을 반환하고, 두 번째 NOT 연산자는 첫 번째 NOT 연산자가 반환한 값의 역을 반환합니다. 이렇게 NOT을 연달아 사용하면 특정 값을 불린형으로 변환할 수 있습니다.

참고로, 내장 함수 Boolean을 사용하면 !!을 사용한 것과 같은 결과를 도출할 수 있습니다.

alert( Boolean("non-empty string") ); // true
alert( Boolean(null) ); // false

NOT 연산자의 우선순위는 모든 논리 연산자 중에서 가장 높기 때문에 항상 &&|| 보다 먼저 실행됩니다.


nullish 병합 연산자 "??"


nullish 병합 연산자(nullish coalescing operator) ??를 사용하면 짧은 문법으로 여러 피연산자 중 그 값이 "확정되어있는" 변수를 찾을 수 있습니다.

a ?? b의 평가 결과는 다음과 같습니다.

📌 anull도 아니고 undefined도 아니면 a
📌 그 외의 경우는 b

nullish 병합 연산자 ??없이 x = a ?? b와 동일한 동작을 하는 코드를 작성하면 다음과 같습니다.

x = (a !== null && a !== undefined) ? a : b;

비교 연산자와 논리 연산자만으로 nullish 병합 연산자와 같은 기능을 하는 코드를 작성하니 코드 길이가 길어지네요.

또 다른 예시를 살펴봅시다. firstName, lastName, nickName이란 변수에 사용자 이름이나 별명을 저장하는데, 사용자가 아무런 정보도 입력하지 않는 케이스도 허용한다고 해보겠습니다.

화면엔 세 변수 중 실제 값이 있는 변수의 값을 출력하는데, 세 변수 모두 값이 없다면 "익명의 사용자"가 출력되도록 해보죠.

이럴 때 nullish 병합 연산자 ??를 사용하면 값이 정해진 변수를 간편하게 찾아낼 수 있습니다.

let firstName = null;
let lastName = null;
let nickName = "바이올렛";

// null이나 undefined가 아닌 첫 번째 피연산자
alert(firstName ?? lastName ?? nickName ?? "익명의 사용자"); // 바이올렛

??와 ||의 차이


nullish 병합 연산자는 OR 연산자와 상당히 유사해 보입니다. 실제로 위 예시에서 ??||로 바꿔도 그 결과는 동일하기까지 하죠.

그런데 두 연산자 사이에는 중요한 차이점이 있습니다.

📌 ||는 첫 번째 truthy 값을 반환합니다.
📌 ??는 첫 번째 정의된(defined) 값을 반환합니다.


🔥 nullundefined, 숫자 0을 구분 지어 다뤄야 할 때 이 차이점은 매우 중요한 역할을 합니다.

height = height ?? 100;

height에 값이 정의되지 않은경우 height100이 할당됩니다.

이제 ??||을 비교해봅시다.

let height = 0;

alert(height || 100); // 100
alert(height ?? 100); // 0

height || 100height0을 할당했지만 0을 falsy 한 값으로 취급했기 때문에 null이나 undefined를 할당한 것과 동일하게 처리합니다. 따라서 height || 100의 평가 결과는 100입니다.

반면 height ?? 100의 평가 결과는 height가 정확하게 null이나 undefined일 경우에만 100이 됩니다. 예시에선 height0이라는 값을 할당했기 때문에 얼럿창엔 0이 출력됩니다.

이런 특징 때문에 높이처럼 0이 할당될 수 있는 변수를 사용해 기능을 개발할 땐 ||보다 ??가 적합합니다.


연산자 우선순위


??의 연산자 우선순위는 5로 꽤 낮습니다.

따라서 ??=? 보다는 먼저, 대부분의 연산자보다는 나중에 평가됩니다.

그렇기 때문에 복잡한 표현식 안에서 ??를 사용해 값을 하나 선택할 땐 괄호를 추가하는 게 좋습니다.

let height = null;
let width = null;

// 괄호를 추가!
let area = (height ?? 100) * (width ?? 50);

alert(area); // 5000

그렇지 않으면 *??보다 우선순위가 높기 때문에 *가 먼저 실행됩니다.

결국엔 아래 예시처럼 동작하겠죠.

// 원치 않는 결과
let area = height ?? (100 * width) ?? 50;

??엔 자바스크립트 언어에서 규정한 또 다른 제약사항이 있습니다.

안정성 관련 이슈 때문에 ??&&||와 함께 사용하지 못합니다.

아래 예시를 실행하면 문법 에러가 발생합니다.

let x = 1 && 2 ?? 3; // SyntaxError: Unexpected token "??"

이 제약에 대해선 아직 논쟁이 많긴 하지만 사람들이 ||??로 바꾸기 시작하면서 만드는 실수를 방지하고자 명세서에 제약이 추가된 상황입니다.

제약을 피하려면 괄호를 사용해주세요.

let x = (1 && 2) ?? 3; // 제대로 동작합니다.

alert(x); // 2
profile
생각 많이 하지 않기 😎

0개의 댓글