Devin.KR
로그인

자바스크립트 연산자와 제어문 - truthy falsy, 옵셔널 체이닝, nullish 병합

개발자 조회 1

이 단원에서 배우는 것

3단원에서 타입이 섞이면 값이 조용히 변한다는 것을 봤다. 조건문은 어떤 값이든 참·거짓으로 바꿔서 판정하기 때문에 그 변환이 가장 자주 일어나는 자리다. 이번 단원에서는 조건과 반복을 쓰면서, 동시에 ES2020에서 들어온 두 연산자(?.??)로 방어 코드를 짧게 만드는 법을 익힌다. 예제는 계속 스터디 참가자 명단이다.

  • falsy 값 목록을 외우고, if (값)이 안전한 경우와 위험한 경우를 구분한다.
  • if·switch·for...of를 상황에 맞게 골라 쓴다.
  • ||??의 차이를 설명하고, 기본값을 줄 때 어느 쪽을 써야 하는지 판단한다.

?.??는 ES2020, ??=는 ES2021에 들어왔다. 최신 브라우저와 Node 20 이상에서는 그대로 쓸 수 있다.

왜 필요한가

서버가 준 참가자 목록은 사람마다 채워진 항목이 다르다. 연락처를 안 적은 사람, 메모가 빈 문자열인 사람, 출석이 0회인 신규 참가자가 섞여 있다.

const member = { name: "박서연" };          // 연락처가 아예 없다
console.log(member.contact.email);          // TypeError: Cannot read properties of undefined

이걸 막으려고 예전에는 이렇게 썼다.

const email = member && member.contact && member.contact.email
  ? member.contact.email
  : "이메일 미등록";

깊이가 한 단계 늘 때마다 조건이 하나씩 붙는다. 게다가 이 방식은 3단원에서 본 falsy 문제를 그대로 안고 있다. 값이 빈 문자열이면 "없는 것"으로 취급해 버린다. ?.??는 정확히 이 두 문제를 각각 해결하려고 만들어진 연산자다.

문법과 예제

falsy 값은 딱 여섯 개다

조건문에 넣었을 때 거짓으로 판정되는 값은 다음이 전부다. 이 목록에 없으면 전부 참이다.

  • false
  • 0 (그리고 -0, 0n)
  • "" 빈 문자열
  • null
  • undefined
  • NaN
console.log(Boolean("0"));      // true  — 글자가 들어 있으므로 참
console.log(Boolean("false"));  // true
console.log(Boolean(" "));      // true  — 공백도 글자다
console.log(Boolean([]));       // true  — 빈 배열도 객체다
console.log(Boolean({}));       // true

마지막 두 줄이 특히 중요하다. "명단이 비었으면 안내 문구를 띄운다"를 if (list)로 쓰면 빈 배열도 참이라 절대 실행되지 않는다. 길이를 봐야 한다.

const list = [];
if (list.length === 0) {
  console.log("아직 참가자가 없습니다");
}

조건문

const sessions = 5;

if (sessions >= 8) {
  console.log("개근");
} else if (sessions >= 4) {
  console.log("성실");
} else {
  console.log("참여 독려 필요");
}

// 값 하나를 고르는 경우엔 삼항 연산자가 짧다
const grade = sessions >= 4 ? "성실" : "독려";
console.log(grade);

값이 정해진 몇 가지 중 하나일 때는 switch가 읽기 좋다. switch의 비교는 ===다. 타입까지 같아야 걸린다.

const status = "paid";

switch (status) {
  case "paid":
    console.log("납부 완료");
    break;
  case "pending":
    console.log("입금 확인 중");
    break;
  default:
    console.log("미납");
}

반복문 — 실무에서 쓰는 것은 사실상 for...of

const members = ["김지훈", "박서연", "이도현"];

for (const name of members) {
  console.log(name);
}

// 순번이 필요하면 entries()
for (const [index, name] of members.entries()) {
  console.log(`${index + 1}. ${name}`);
}

// 인덱스를 직접 다뤄야 할 때만 고전적인 for
for (let i = 0; i < members.length; i++) {
  console.log(i, members[i]);
}
1. 김지훈
2. 박서연
3. 이도현

두 번째 예제에서 대괄호로 두 값을 한꺼번에 받는 문법은 구조 분해라고 하며 6단원에서 제대로 다룬다. 지금은 entries()가 "순번과 값"을 짝으로 준다는 것만 알면 된다.

중간에 멈추려면 break, 이번 회차만 건너뛰려면 continue다. 6단원에서 배열 전용 도구(filter, map)를 배우면 반복문을 직접 쓸 일이 더 줄어든다.

&& 와 || 는 불리언을 돌려주지 않는다

console.log(true && "김지훈");    // "김지훈"
console.log(null || "이름 없음");  // "이름 없음"
console.log("김지훈" || "이름 없음"); // "김지훈"

||는 왼쪽이 truthy면 왼쪽을, 아니면 오른쪽을 돌려준다. &&는 반대다. 두 연산자는 결과가 정해지는 순간 오른쪽을 아예 평가하지 않는다(단축 평가). 이 성질 때문에 조건 없이 기본값을 줄 때 오래 쓰였는데, 바로 다음 항목이 그 한계다.

?? — 없을 때만 기본값

const member = { name: "김지훈", sessions: 0, memo: "" };

console.log(member.sessions || 10);   // 10  ← 0 이 falsy 라서 기본값이 끼어들었다
console.log(member.sessions ?? 10);   // 0   ← 원하던 결과

console.log(member.memo || "메모 없음");  // "메모 없음"
console.log(member.memo ?? "메모 없음");  // ""  ← 빈 메모를 그대로 유지

??는 왼쪽이 null 또는 undefined일 때만 오른쪽을 쓴다. 0과 빈 문자열이 정상 값인 자리에서는 반드시 ??를 쓴다. 출석 횟수, 금액, 수량, 메모처럼 0과 빈 값이 의미를 갖는 항목이 전부 여기 해당한다.

const config = { limit: 0 };
config.limit ??= 20;      // null/undefined 일 때만 대입한다
console.log(config.limit);  // 0 — 이미 값이 있으므로 그대로

?. — 없으면 멈추고 undefined

const member = { name: "박서연" };

console.log(member.contact?.email);                    // undefined (오류 없음)
console.log(member.contact?.email ?? "이메일 미등록");   // 이메일 미등록

const other = { name: "김지훈", contact: { email: "jihun@example.com" } };
console.log(other.contact?.email);   // jihun@example.com

?.는 왼쪽이 null이나 undefined면 그 자리에서 undefined를 내고 멈춘다. 배열 인덱스와 함수 호출에도 쓸 수 있는데 모양이 조금 다르다는 점을 주의한다.

console.log(member.tags?.[0]);    // 배열 접근 — 대괄호 앞에 점이 붙는다
console.log(member.notify?.());   // 함수 호출 — 괄호 앞에 점이 붙는다

?.??는 짝으로 쓰는 경우가 많다. "없으면 넘어가고, 최종적으로 없으면 기본값"이 대부분의 화면 코드가 필요로 하는 동작이다.

실무에서 자주 틀리는 것

1. 기본값을 || 로 준다

가장 흔하고 가장 오래 안 들키는 버그다. (아래 예제의 function은 5단원에서 다룬다. 여기서는 참가자 한 명을 받아 한 줄짜리 문자열을 만들어 주는 코드로 읽으면 된다.)

function renderRow(member) {
  const sessions = member.sessions || "-";      // 출석 0회가 "-" 로 표시된다
  const memo = member.memo || "메모 없음";       // 일부러 비운 메모가 "메모 없음" 이 된다
  return `${member.name} / 출석 ${sessions} / ${memo}`;
}
console.log(renderRow({ name: "이도현", sessions: 0, memo: "" }));
// 이도현 / 출석 - / 메모 없음

출석 0회는 실재하는 값이다. 이 화면을 본 운영자는 "신규 참가자 데이터가 안 들어온다"고 보고할 것이고, 개발자는 서버 쪽을 뒤지게 된다. ??로 바꾸면 이도현 / 출석 0 / 이 나온다. 문자열 이름처럼 빈 값이 의미 없는 항목에만 ||를 쓴다.

2. ?. 을 체인 중간에만 붙인다

const member = { name: "박서연" };

console.log(member.tags?.[0]);   // undefined — 안전
console.log(member.tags[0]);     // TypeError: Cannot read properties of undefined

반대로 한 번 ?.이 걸리면 그 뒤 체인 전체가 건너뛰어진다는 점도 알아 둬야 한다.

console.log(member.contact?.email.toUpperCase());   // undefined — 오류가 아니다

email.toUpperCase()에는 ?.이 없는데도 오류가 안 난다. member.contactundefined인 순간 나머지를 평가하지 않기 때문이다. 다만 contact는 있는데 email만 없는 경우에는 여전히 터진다. "없을 수 있는 지점"마다 붙여야 한다는 뜻이다.

3. ?. 을 오류 감추개로 쓴다

오류가 나면 일단 ?.을 붙여서 넘어가는 습관이 생기기 쉽다. 그런데 ?.은 "없어도 정상"인 자리에만 써야 한다. 반드시 있어야 하는 값에 붙이면, 데이터가 잘못 들어온 사실이 감춰지고 화면에 undefined만 조용히 표시된다. 원인 지점에서 터졌어야 할 오류가 열 단계 뒤로 밀리는 셈이라 디버깅이 훨씬 어려워진다.

// 나쁜 예 — id 는 반드시 있어야 하는 값이다
const id = member?.id;

// 좋은 예 — 없으면 그 자리에서 문제를 드러낸다
if (member.id === undefined) {
  throw new Error("참가자 id 가 없습니다");
}

4. 배열을 for...in 으로 돈다

const members = ["김지훈", "박서연"];

for (const i of members.keys()) {
  console.log(typeof i, i);      // number 0 / number 1
}

for (const i in members) {
  console.log(typeof i, i);      // string 0 / string 1   ← 인덱스가 문자열이다
}

for...in객체의 키를 도는 문법이라 배열에 쓰면 인덱스가 문자열로 나온다. i + 1을 하면 3단원에서 본 "01"이 나오고, 순서 보장도 명세상 완전하지 않으며, 누군가 배열에 추가한 속성까지 딸려 온다. 배열은 for...of, 객체는 Object.entries(6단원)로 돈다.

5. switch 에서 break 를 빠뜨린다

break가 없으면 다음 case의 코드까지 이어서 실행된다. 문법 오류가 아니라 의도한 문법(fall-through)이라 아무도 경고해 주지 않는다. 조건이 서너 개를 넘어가면 switch 대신 객체로 매핑하는 편이 안전한 경우도 많다.

스스로 확인하기

  1. 다음 출력은 무엇이며, 두 줄이 다른 이유는?
    const member = { name: "이도현", sessions: 0 };
    console.log(member.sessions || "미출석");
    console.log(member.sessions ?? "미출석");
  2. 참가자 목록이 비었을 때 안내 문구를 띄우려 한다. 다음 코드가 절대 실행되지 않는 이유와 고친 코드를 쓰라.
    const list = [];
    if (!list) {
      console.log("아직 참가자가 없습니다");
    }
  3. membercontact가 있을 수도 없을 수도 있고, 있어도 phone이 없을 수 있다. 전화번호가 없으면 "연락처 없음"을 출력하는 한 줄을 ?.??를 써서 작성하라.

정답

  1. 첫 줄은 미출석, 둘째 줄은 0. 0은 falsy라 ||의 왼쪽이 거짓으로 판정되어 오른쪽이 선택된다. ??nullundefined만 걸러내므로 0을 그대로 통과시킨다.
  2. 빈 배열도 객체이므로 truthy이고, !list는 항상 false다. if (list.length === 0)으로 고친다.
  3. console.log(member.contact?.phone ?? "연락처 없음");contact가 없으면 ?.undefined를 내고, ??가 그것을 받아 기본값으로 바꾼다.