퀴즈 서비스를 운영하던 어느 날, 항의 메일이 도착했습니다. “저 방금 시험 봤는데 왜 ‘점수 없음’이라고 나오죠?” 확인해보니 그분의 점수는 0점. 분명히 데이터베이스에는 0이 저장돼 있었고, API도 0을 잘 돌려주고 있었습니다. 범인은 화면 코드의 딱 한 줄이었어요.
const score = userScore || "점수 없음";||는 왼쪽 값이 falsy면 오른쪽을 반환합니다. falsy는 조건문에서 거짓처럼 취급되는 값을 부르는 말인데(MDN 용어집 참고), 자바스크립트에서는 0도 여기에 포함됩니다. false, "", null, undefined, NaN도 마찬가지예요. 의도한 값인 0이 기본값으로 교체되는 버그, 생각보다 자주 발생합니다.

사진: Unsplash의 Erik Mclean
||의 동작 원리: falsy면 오른쪽#
||는 왼쪽이 falsy일 때 오른쪽 값을 반환합니다.
console.log(null || "기본값"); // "기본값"
console.log(undefined || "기본값"); // "기본값"
console.log(0 || "기본값"); // "기본값" ← 의도하지 않은 동작
console.log("" || "기본값"); // "기본값" ← 의도하지 않은 동작
console.log(false || "기본값"); // "기본값" ← 의도하지 않은 동작
자바스크립트의 falsy 값 목록:
| 값 | 타입 |
|---|---|
false | Boolean |
0, -0, 0n | Number, BigInt |
"", '', `` | String (빈 문자열) |
null | Null |
undefined | Undefined |
NaN | Number |
이 중 0, "", false는 의미 있는 값일 수 있습니다. 이게 ||의 함정이에요.
null과 undefined는 그렇다 쳐도, 숫자 0이 “없음"이라니. 자바스크립트가 처음 만들어질 때 이 결정을 내린 분이 지금은 어떤 심정일지 궁금합니다.
??의 동작 원리: null/undefined일 때만#
??(nullish coalescing)은 왼쪽 값이 null 또는 undefined일 때만 오른쪽을 반환합니다.
console.log(null ?? "기본값"); // "기본값" ✅
console.log(undefined ?? "기본값"); // "기본값" ✅
console.log(0 ?? "기본값"); // 0 ✅ 그대로 통과
console.log("" ?? "기본값"); // "" ✅ 그대로 통과
console.log(false ?? "기본값"); // false ✅ 그대로 통과
null과 undefined만 “값이 없는 상태"로 취급하고, 나머지는 모두 유효한 값으로 봅니다. 이 둘(null과 undefined)을 묶어 부르는 이름이 nullish예요 — 연산자 이름이 여기서 왔습니다.

참고로, 둘을 섞으면 문법 에러입니다#
||와 ??를 괄호 없이 한 식에 쓰면 어떻게 될까요? 실행조차 안 됩니다.
const value = a || b ?? c; // SyntaxError!
const value = (a || b) ?? c; // 괄호를 붙이면 OK
버그가 아니라 일부러 이렇게 설계됐어요. 두 연산자의 우선순위를 사람마다 다르게 기억할 게 뻔하니, 명세가 “싸우지 말고 괄호로 의도를 적어라"라고 심판을 본 겁니다. 언어가 강제하는 코드 리뷰인 셈이죠.
실무에서 자주 만나는 패턴#
숫자 설정값#
// 사용자가 타임아웃을 0으로 설정할 수 있는 경우
function createTimer(timeout) {
// ❌ 0을 설정하면 기본값 3000으로 대체됨
const delay = timeout || 3000;
// ✅ 0도 유효한 값으로 처리
const delay = timeout ?? 3000;
}빈 문자열 처리#
// 사용자가 설명을 의도적으로 비워둘 수 있는 경우
const description = userInput || "설명이 없습니다"; // ❌ 빈 문자열도 기본값으로
const description = userInput ?? "설명이 없습니다"; // ✅ null/undefined만 기본값으로
API 응답의 기본값#
const response = await fetchUserSettings();
// ❌ fontSize가 0이면 14로 덮어씌워짐
const fontSize = response.fontSize || 14;
// ✅ 명시적으로 null/undefined인 경우만 기본값 적용
const fontSize = response.fontSize ?? 14;Optional Chaining과 함께 쓰기#
??은 ?.(optional chaining)과 함께 쓸 때 진가를 발휘합니다.
const user = {
profile: {
age: 0, // 0세 사용자 (드물지만 유효한 값)
},
};
// ❌ age가 0이면 "나이 정보 없음"으로 표시됨
const age = user?.profile?.age || "나이 정보 없음";
// ✅ null/undefined인 경우만 기본값 표시
const age = user?.profile?.age ?? "나이 정보 없음";user?.profile?.age가 undefined(프로퍼티 없음)일 때만 “나이 정보 없음"을 보여주고, 0이 오면 그대로 0을 씁니다.
??= 할당 연산자#
??=는 값이 null 또는 undefined일 때만 할당합니다. 설정 초기화에 유용해요.
const config = {
theme: "dark",
language: null,
};
// null/undefined인 경우만 기본값 설정
config.language ??= "ko"; // null이었으니 "ko" 할당
config.theme ??= "light"; // "dark"가 있으니 그대로
console.log(config);
// { theme: "dark", language: "ko" }
기존에는 이걸 이렇게 썼어야 했죠.
config.language = config.language ?? "ko";??=로 훨씬 깔끔해집니다.
언제 ||를 써야 할까?#
??가 항상 좋은 건 아닙니다. 0, "", false도 기본값으로 교체해야 하는 경우가 있어요.
// 입력값이 없거나 비어있으면 기본 검색어 사용
const query = searchInput || "전체"; // "" → "전체"로 교체 의도적
// 권한이 없는 사용자에게 기본 역할 부여
const role = userRole || "guest"; // false/undefined/null 모두 "guest"
기준은 이렇게 생각하면 됩니다:
0,"",false도 “값이 없다"고 봐야 하면 →||null/undefined만 “값이 없다"고 봐야 하면 →??

헷갈리면 ??가 안전한 기본기입니다 — 실수해도 “기본값이 안 나오는” 쪽이지, 사용자의 0점이 증발하는 쪽은 아니니까요.
정리#
||: falsy 값(0,"",false,null,undefined등) 이면 기본값 반환??:null또는undefined일 때만 기본값 반환??=: 변수가null/undefined일 때만 할당
코드에서 기본값을 설정할 때, “0이나 빈 문자열도 유효한 값인가?“를 먼저 생각해보세요. 그 답이 ||와 ?? 중 어떤 걸 쓸지 알려줍니다.
그 항의 메일의 답장은 한 글자 수정이었습니다 — ||를 ??로. 0점을 받은 그분께는 죄송하지만, 덕분에 이 글이 나왔네요.
