# `??` vs `||`, 0이 사라지는 버그의 원인

> ?? 연산자(nullish coalescing)와 ||의 결정적 차이 — 0과 빈 문자열이 기본값에 먹히는 버그의 원인입니다. falsy의 함정, ??= 할당, 옵셔널 체이닝 조합, 그리고 둘을 섞으면 SyntaxError가 나는 이유까지 정리했습니다.

**Published:** 2026-07-14 | **Updated:** 2026-07-14

---


퀴즈 서비스를 운영하던 어느 날, 항의 메일이 도착했습니다. "저 방금 시험 봤는데 왜 '점수 없음'이라고 나오죠?" 확인해보니 그분의 점수는 **0점**. 분명히 데이터베이스에는 `0`이 저장돼 있었고, API도 `0`을 잘 돌려주고 있었습니다. 범인은 화면 코드의 딱 한 줄이었어요.

```javascript
const score = userScore || "점수 없음";
```

`||`는 왼쪽 값이 **falsy**면 오른쪽을 반환합니다. 그런데 자바스크립트에서 `0`은 falsy입니다. `false`, `""`, `null`, `undefined`, `NaN`도 마찬가지예요. 의도한 값인 `0`이 기본값으로 교체되는 버그, 생각보다 자주 발생합니다.

{{< img src="images/contents/zero-score.jpg" alt="아케이드 게임기 점수판에 YOUR SCORE 0이 표시된 모습 - 0점은 엄연히 존재하는 점수인데, 자바스크립트의 || 연산자는 0을 falsy로 보고 기본값으로 바꿔버립니다" caption="사진: <a href='https://unsplash.com/ko/사진/밤중에-아케이드-기계-근처에-서-있는-남자와-여자-gbfqWoaTmdg' target='_blank' title='새 창에서 열림'>Unsplash</a>의 <a href='https://unsplash.com/ko/@introspectivedsgn' target='_blank' title='새 창에서 열림'>Erik Mclean</a>" >}}

## `||`의 동작 원리: falsy면 오른쪽

`||`는 **왼쪽이 falsy일 때** 오른쪽 값을 반환합니다.

```javascript
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`일 때만** 오른쪽을 반환합니다.

```javascript
console.log(null ?? "기본값");      // "기본값" ✅
console.log(undefined ?? "기본값"); // "기본값" ✅
console.log(0 ?? "기본값");         // 0        ✅ 그대로 통과
console.log("" ?? "기본값");        // ""       ✅ 그대로 통과
console.log(false ?? "기본값");     // false    ✅ 그대로 통과
```

`null`과 `undefined`만 "값이 없는 상태"로 취급하고, 나머지는 모두 유효한 값으로 봅니다. 이 둘(`null`과 `undefined`)을 묶어 부르는 이름이 **nullish**예요 — 연산자 이름이 여기서 왔습니다.

{{< img src="images/contents/filter-concept.png" alt="||와 ?? 두 필터의 비교 다이어그램 - || 필터는 0, 빈 문자열, false까지 전부 걸러내 기본값으로 바꾸지만, ?? 필터는 null과 undefined만 걸러내고 0, 빈 문자열, false는 유효한 값으로 통과시킵니다" >}}

### 참고로, 둘을 섞으면 문법 에러입니다

`||`와 `??`를 괄호 없이 한 식에 쓰면 어떻게 될까요? 실행조차 안 됩니다.

```javascript
const value = a || b ?? c;   // SyntaxError!
const value = (a || b) ?? c; // 괄호를 붙이면 OK
```

버그가 아니라 **일부러 이렇게 설계**됐어요. 두 연산자의 우선순위를 사람마다 다르게 기억할 게 뻔하니, 명세가 "싸우지 말고 괄호로 의도를 적어라"라고 심판을 본 겁니다. 언어가 강제하는 코드 리뷰인 셈이죠.

## 실무에서 자주 만나는 패턴

### 숫자 설정값

```javascript
// 사용자가 타임아웃을 0으로 설정할 수 있는 경우
function createTimer(timeout) {
  // ❌ 0을 설정하면 기본값 3000으로 대체됨
  const delay = timeout || 3000;

  // ✅ 0도 유효한 값으로 처리
  const delay = timeout ?? 3000;
}
```

### 빈 문자열 처리

```javascript
// 사용자가 설명을 의도적으로 비워둘 수 있는 경우
const description = userInput || "설명이 없습니다"; // ❌ 빈 문자열도 기본값으로
const description = userInput ?? "설명이 없습니다"; // ✅ null/undefined만 기본값으로
```

### API 응답의 기본값

```javascript
const response = await fetchUserSettings();

// ❌ fontSize가 0이면 14로 덮어씌워짐
const fontSize = response.fontSize || 14;

// ✅ 명시적으로 null/undefined인 경우만 기본값 적용
const fontSize = response.fontSize ?? 14;
```

## Optional Chaining과 함께 쓰기

`??`은 `?.`(optional chaining)과 함께 쓸 때 진가를 발휘합니다.

```javascript
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`일 때만 할당합니다. 설정 초기화에 유용해요.

```javascript
const config = {
  theme: "dark",
  language: null,
};

// null/undefined인 경우만 기본값 설정
config.language ??= "ko";    // null이었으니 "ko" 할당
config.theme ??= "light";    // "dark"가 있으니 그대로

console.log(config);
// { theme: "dark", language: "ko" }
```

기존에는 이걸 이렇게 썼어야 했죠.

```javascript
config.language = config.language ?? "ko";
```

`??=`로 훨씬 깔끔해집니다.

## 언제 `||`를 써야 할까?

`??`가 항상 좋은 건 아닙니다. `0`, `""`, `false`도 기본값으로 교체해야 하는 경우가 있어요.

```javascript
// 입력값이 없거나 비어있으면 기본 검색어 사용
const query = searchInput || "전체"; // "" → "전체"로 교체 의도적

// 권한이 없는 사용자에게 기본 역할 부여
const role = userRole || "guest"; // false/undefined/null 모두 "guest"
```

**기준은 이렇게 생각하면 됩니다:**

- `0`, `""`, `false`도 "값이 없다"고 봐야 하면 → `||`
- `null`/`undefined`만 "값이 없다"고 봐야 하면 → `??`

{{< img src="images/contents/when-or-nullish.png" alt="||와 ?? 선택 가이드 다이어그램 - 기본값을 넣기 전에 0이나 빈 문자열, false도 유효한 값인지 묻고, 유효하면 ??를, 없는 것으로 취급해야 하면 ||를 고르는 갈림길입니다. 확신이 없다면 ??가 데이터를 덜 잃는 안전한 기본기라는 안내가 붙어 있습니다" >}}

헷갈리면 `??`가 안전한 기본기입니다 — 실수해도 "기본값이 안 나오는" 쪽이지, **사용자의 0점이 증발하는** 쪽은 아니니까요.

## 정리

- **`||`**: falsy 값(`0`, `""`, `false`, `null`, `undefined` 등) 이면 기본값 반환
- **`??`**: `null` 또는 `undefined`일 때만 기본값 반환
- **`??=`**: 변수가 `null`/`undefined`일 때만 할당

코드에서 기본값을 설정할 때, "0이나 빈 문자열도 유효한 값인가?"를 먼저 생각해보세요. 그 답이 `||`와 `??` 중 어떤 걸 쓸지 알려줍니다.

그 항의 메일의 답장은 한 글자 수정이었습니다 — `||`를 `??`로. 0점을 받은 그분께는 죄송하지만, 덕분에 이 글이 나왔네요.

---

{{< faq >}}

## 이 시리즈의 다른 글

- [함수 호이스팅, 선언보다 위에서 불러도 되는 이유]({{< relref "javascript-function-hoisting" >}})
- [`Promise.all` vs `Promise.allSettled`, 실수하기 쉬운 차이]({{< relref "javascript-promise-methods" >}})

