자바스크립트 ?? vs || 완벽 정리: null, undefined, 0, 빈 문자열을 올바르게 처리하는 법

볼륨 0이 50으로 바뀌고, 빈 닉네임이 '익명'이 되는 버그는 대부분 || 때문이다. ??와 ||의 차이, ?. 옵셔널 체이닝, ??=, 기본 매개변수가 적용되는 조건, null 검사하는 올바른 방법까지 직접 값을 넣어보며 정리했다.

볼륨을 0으로 했는데 왜 50이 되지?

사용자 설정을 불러와서, 값이 없으면 기본값을 쓰는 흔한 코드다.

const settings = { volume: 0, nickname: "", darkMode: false };

const volume = settings.volume || 50;
const nickname = settings.nickname || "익명";
const darkMode = settings.darkMode || true;

결과는 이렇다.

50      // 사용자가 일부러 0으로 맞춘 볼륨이 50으로
"익명"  // 비워둔 닉네임이 "익명"으로
true    // 꺼둔 다크 모드가 켜짐

사용자가 고른 값 세 개가 전부 무시됐다. 원인은 ||이고, 해결책은 ??다.

const volume = settings.volume ?? 50; // 0
const nickname = settings.nickname ?? "익명"; // ""
const darkMode = settings.darkMode ?? true; // false

||와 ??의 차이

||는 왼쪽 값이 falsy면 오른쪽을 쓴다. ??(널 병합 연산자, Nullish Coalescing)는 왼쪽 값이 null 또는 undefined일 때만 오른쪽을 쓴다.

자바스크립트에서 falsy 값은 false, 0, ""(빈 문자열), NaN, null, undefined다. ||는 이 여섯 개를 모두 "값 없음"으로 취급한다. 그런데 0, "", false는 대부분 정상적인 값이다.

왼쪽 값값 || "기본값"값 ?? "기본값"
0"기본값"0
"""기본값"""
false"기본값"false
NaN"기본값"NaN
null"기본값""기본값"
undefined"기본값""기본값"

아래 미리보기에서 값을 하나씩 눌러보며 직접 비교해보자.

기준은 하나다. "0, 빈 문자열, false도 정상적인 값인가?" 그렇다면 ??, 아니라면(빈 값도 기본값으로 바꾸고 싶다면) ||를 쓴다. 대부분의 설정값, 숫자, 폼 입력에는 ??가 맞다.


?. 옵셔널 체이닝: 중간에 값이 없어도 터지지 않게

API 응답처럼 중간 단계가 비어 있을 수 있는 객체를 다룰 때 가장 흔한 에러가 이것이다.

const user = { profile: null };
user.profile.name;
// TypeError: Cannot read properties of null (reading 'name')

?.(옵셔널 체이닝, Optional Chaining)는 앞의 값이 null이나 undefined면 에러 대신 undefined를 돌려주고 멈춘다.

user.profile?.name; // undefined
user.profile?.name ?? "이름 없음"; // "이름 없음"

?.와 ??는 짝꿍처럼 함께 쓰인다. ?.로 안전하게 꺼내고, ??로 기본값을 채운다.

쓰는 곳문법예시
속성obj?.propuser?.name
대괄호 접근obj?.[key]order.items?.[0]
함수 호출fn?.()options.onSuccess?.()
// 콜백이 있을 때만 호출
options.onSuccess?.(result);

// 장바구니 첫 상품 가격, 없으면 0
const firstPrice = order.items?.[0]?.price ?? 0;

?.를 모든 점(.)마다 습관적으로 붙이지 말자. res?.data?.user?.address?.city처럼 쓰면, 절대 비어 있으면 안 되는 값이 비어 있어도 조용히 undefined가 되어 버그가 숨는다. 실제로 비어 있을 수 있는 단계에만 붙여야, 있어야 할 값이 없을 때 에러로 빨리 발견할 수 있다.


??=, ||=, &&=: 값이 없을 때만 채우기

논리 할당 연산자(Logical Assignment)는 "조건이 맞을 때만 대입"을 짧게 쓴다.

const config = { retries: 0, timeout: undefined };

config.retries ??= 3; // 0은 그대로 (null/undefined가 아니니까)
config.timeout ??= 5000; // undefined라서 5000이 들어감
// { retries: 0, timeout: 5000 }
연산자대입하는 조건풀어 쓰면
a ??= ba가 null/undefined일 때a ?? (a = b)
a ||= ba가 falsy일 때a || (a = b)
a &&= ba가 truthy일 때a && (a = b)

여기서도 같은 함정이 있다. config.retries ||= 3이라고 쓰면 사용자가 일부러 정한 0(재시도 안 함)이 3으로 바뀐다.


기본 매개변수와 구조 분해 기본값은 undefined일 때만

함수 매개변수의 기본값은 ??보다도 범위가 좁다. undefined일 때만 적용되고, null은 그대로 들어온다.

function greet(name = "손님") {
  return `안녕하세요, ${name}!`;
}

greet(); // "안녕하세요, 손님!"
greet(undefined); // "안녕하세요, 손님!"
greet(null); // "안녕하세요, null!"  ← 기본값이 적용되지 않음
greet(""); // "안녕하세요, !"

구조 분해(Destructuring)의 기본값도 똑같다.

const { page = 1, size = 10 } = { page: null, size: undefined };
page; // null (기본값 적용 안 됨)
size; // 10

API 응답에는 null이 흔히 섞여 온다. null까지 기본값으로 바꾸고 싶다면 기본 매개변수 대신 ??를 쓰자.

function greet(name) {
  return `안녕하세요, ${name ?? "손님"}!`;
}
방법undefinednull0, "", false
기본 매개변수 / 구조 분해 기본값기본값그대로그대로
??기본값기본값그대로
||기본값기본값기본값

값이 "없는지" 올바르게 검사하기

null과 undefined를 한 번에: == null

평소에는 ===를 쓰는 게 원칙이지만, null과 undefined를 함께 검사할 때만큼은 == null이 짧고 정확하다.

value == null; // value가 null 또는 undefined일 때만 true

0 == null; // false
"" == null; // false
false == null; // false

if (!value)는 0과 ""까지 걸러버리니, "값이 비어 있는지"를 볼 때는 value == null이 의도에 더 가깝다.

헷갈리는 타입 검사

하고 싶은 것❌ 틀리기 쉬운 방법✅ 올바른 방법
배열인지typeof arr === "object"Array.isArray(arr)
null인지typeof v === "object" (null도 "object")v === null
NaN인지v === NaN (항상 false), isNaN(v) (문자열 "abc"도 true)Number.isNaN(v)

??는 NaN을 걸러주지 않는다

숫자 변환과 ??를 같이 쓸 때 자주 하는 실수다.

const qty = Number("abc"); // NaN
qty ?? 1; // NaN  ← NaN은 null/undefined가 아니라서 그대로
Number.isNaN(qty) ? 1 : qty; // 1

Number("")와 Number(null)은 NaN이 아니라 0이 된다는 것도 기억해두자. 빈 입력칸이 조용히 0으로 바뀌는 버그의 원인이다.

??와 ||는 괄호 없이 섞을 수 없다

null || undefined ?? "x"; // SyntaxError
(null || undefined) ?? "x"; // "x"

어느 쪽을 먼저 계산할지 헷갈리지 않도록 언어 차원에서 막아둔 것이다. 섞어야 한다면 괄호로 순서를 명확히 하자.


정리: 왜 알아두면 좋은가

상황올바른 처리
기본값 넣기 (0, "", false도 유효한 값)값 ?? 기본값
빈 값까지 전부 기본값으로값 || 기본값
중간이 비어 있을 수 있는 객체 접근obj?.a?.b
있을 때만 콜백 호출fn?.()
비어 있을 때만 채우기a ??= b
null까지 기본값 처리하는 매개변수기본 매개변수 대신 ??
null/undefined 검사value == null
배열 / NaN 검사Array.isArray / Number.isNaN

0, 빈 문자열, false는 "값이 없음"이 아니라 사용자가 고른 값인 경우가 많다. ||로 기본값을 넣는 습관은 이런 값을 조용히 지워버리고, 이런 버그는 에러가 나지 않아서 늦게 발견된다. 기본값을 넣을 때는 ??를 먼저 떠올리고, ||는 "빈 값도 기본값으로 바꾸고 싶다"는 의도가 분명할 때만 쓰자.