더 똑똑한 기본값 설정
자바스크립트에서 기본값을 지정할 때 예전부터 쓰던 패턴이 있습니다. 바로 value || fallback 이죠. 대부분 잘 동작하지만, 구조상 함정이 하나 숨어 있습니다. falsy한 값은 전부 "값이 없다"고 간주해 버린다는 점입니다. 그래서 0, '', false처럼 멀쩡한 값이 들어와도 fallback으로 넘어가 버립니다.
이 문제를 해결해 주는 것이 ES2020에서 도입된 널 병합 연산자 ?? 입니다. 이름 그대로 오직 null이나 undefined일 때만 fallback 값을 사용합니다.
0과 ''은 그대로 살아남는다. null과 undefined만 걸러낸다. 널 병합 연산자는 딱 이게 전부다.
|| 연산자로는 왜 부족할까
?? 연산자가 등장한 이유는 아래와 같은 고전적인 버그를 막기 위해서다:
사용자가 볼륨을 0(무음)으로 설정하고 닉네임을 빈 문자열로 지정했는데, 두 값 모두 조용히 덮어써졌습니다. ||는 "값이 없음"과 "falsy한 값"을 구분하지 못하기 때문이죠.
이럴 때는 ?? 연산자로 바꿔주면 됩니다:
이제 0과 ''은 그대로 통과하고, 정말로 값이 없는 필드에만 기본값이 채워집니다. 대부분의 경우 이게 바로 우리가 원하는 동작이죠.
개념 잡기
?? 연산자는 딱 하나만 묻습니다. 왼쪽 값이 null 또는 undefined인가? 그 외엔 전부 통과입니다.
| 왼쪽 값 | left || right 결과 | left ?? right 결과 |
|---|---|---|
null | right | right |
undefined | right | right |
0 | right | left (0) |
'' | right | left ('') |
false | right | left (false) |
NaN | right | left (NaN) |
| 객체 | left | left |
0이나 빈 문자열처럼 의미 있는 falsy 값이 섞여 있는 데이터라면 ??를 쓰세요. 반대로 짧은 문자열이나 0 같은 falsy 값을 전부 걸러내고 싶을 때만 ||가 맞습니다. 그런 상황이 아예 없는 건 아니지만, 흔히 생각하는 것보다 훨씬 드뭅니다.
단락 평가(Short-Circuiting)
||, &&와 마찬가지로 ??도 단락 평가됩니다. 왼쪽 값이 nullish가 아니면 오른쪽 식은 아예 실행되지 않습니다.
expensiveDefault()은 딱 한 번, b에서만 실행됩니다. 폴백이 함수 호출이나 네트워크 요청처럼 굳이 필요 없을 땐 건너뛰고 싶은 작업일 때 아주 유용하죠.
옵셔널 체이닝과 함께 쓰기
??와 ?.는 ES2020에서 함께 도입된 만큼, 짝꿍처럼 같이 쓰라고 만든 연산자입니다. 옵셔널 체이닝은 중간에 값이 없을 수도 있는 경로를 따라가다가 어느 단계든 비어 있으면 undefined를 반환하고, 그 뒤를 널 병합 연산자가 받아 적당한 기본값으로 채워주는 구조죠:
이 둘이 없으면 똑같은 코드가 && 체크가 줄줄이 이어진 지저분한 형태가 되거나 try/catch 범벅이 되기 쉽습니다. 반면 이 둘을 쓰면 안전한 접근과 합리적인 기본값 설정을 한 줄로 깔끔하게 처리할 수 있죠.
널 병합 할당 연산자: ??=
a ??= b는 a가 null이거나 undefined일 때만 b를 a에 할당합니다. 이른바 논리 널 병합 할당 연산자인데, 단축 평가(short-circuit) 방식으로 동작합니다. 즉, a에 이미 값이 들어 있다면 우변은 아예 평가조차 되지 않습니다.
verbose: false나 retries: 0 같은 값들이 그대로 유지된다는 점에 주목하세요. ??=는 말 그대로 "비어 있는" 값만 채워 넣습니다. 반면 ||=를 썼다면 이 두 값도 덮어써 버렸을 거예요.
연산자 우선순위와 괄호 규칙
?? 연산자는 의도적으로 ||나 &&와 괄호 없이 섞여 쓰이는 걸 허용하지 않습니다. 아래 코드는 SyntaxError를 발생시킵니다:
const x = a || b ?? c; // SyntaxError
언어 설계자들은 이 부분의 우선순위가 헷갈릴 여지가 크다고 판단해서, 개발자가 직접 명확하게 괄호로 묶어주도록 강제했습니다. 괄호만 추가해주면 문제없이 동작합니다:
괄호를 어떻게 묶느냐에 따라 결과가 완전히 달라집니다. 이 괄호는 그냥 보기 좋으라고 넣은 게 아니라, 나중에 코드를 읽을 사람(그리고 파서)에게 "내 의도는 이거다"라고 알려주는 장치예요.
기본 매개변수와는 다릅니다
함수 시그니처에 쓰는 기본 매개변수(default parameter)는 나름의 규칙이 있습니다. 인자가 undefined일 때만 동작하고, null에는 반응하지 않아요. 반면 ?? 연산자는 null과 undefined를 똑같이 취급하죠. 미묘하지만 꽤 중요한 차이입니다.
null도 fallback이 동작하게 하고 싶다면, 함수 본문 안에서 ??를 쓰면 됩니다:
작은 차이지만, API가 "값 없음"을 null로 돌려줄 때 헷갈리기 딱 좋은 지점이죠.
?? 연산자는 언제 써야 할까
특별한 이유가 없다면 기본적으로 ?? 연산자를 쓰는 걸 추천합니다. 실제 데이터가 동작하는 방식과 가장 잘 맞거든요. 0, '', false는 대부분 의미 있는 실제 값이라 그대로 유지해야 하고, 정말로 값이 없을 때(null이나 undefined)만 기본값으로 대체하는 게 자연스럽습니다. ||는 falsy 값이면 뭐든 다 바꾸고 싶을 때만 아껴서 쓰세요.
다음 주제: 클래스
객체와 배열은 여기까지입니다. 이제 데이터 구조를 안전하게 다루고 탐색하는 도구는 충분히 챙겼어요. 다음 장에서는 동작을 어떻게 조직화하는지로 넘어갑니다. 클래스, 상속, 그리고 그 밑바탕에 깔린 프로토타입 시스템을 살펴볼 거예요.
자주 묻는 질문
자바스크립트의 널 병합 연산자(??)가 뭔가요?
??는 왼쪽 값이 null이나 undefined일 때만 오른쪽 값을 반환하는 연산자입니다. 그 외의 경우에는 왼쪽 값을 그대로 돌려줍니다. value ?? fallback 형태로 쓰면 0이나 빈 문자열처럼 falsy지만 유효한 값을 기본값으로 덮어쓰는 실수 없이 안전하게 기본값을 지정할 수 있습니다.
?? 와 || 연산자는 어떻게 다른가요?
||는 falsy한 값 전부(0, '', false, NaN, null, undefined)에 대해 기본값으로 넘어갑니다. 반면 ??는 오직 null과 undefined에만 반응합니다. 즉 0이나 빈 문자열이 데이터상 의미 있는 값이라면 ??를 써야 합니다. 반대로 모든 falsy 값을 걸러내고 싶다면 ||가 여전히 정답입니다.
옵셔널 체이닝(?.)과 함께 써도 되나요?
네, 사실상 단짝처럼 같이 씁니다. user?.settings?.theme ?? 'light'처럼 쓰면 중간 어디선가 값이 없어도 안전하게 타고 내려가 undefined가 나오고, 그 자리를 'light'가 채워줍니다. 두 연산자는 이 패턴을 지원하려고 ES2020에서 함께 도입됐습니다.
??= 연산자는 어떤 역할을 하나요?
a ??= b는 a가 null 또는 undefined일 때만 b를 a에 할당합니다. 이른바 논리 널 병합 할당(logical nullish assignment)으로, a = a ?? b의 축약형이지만 단축 평가가 적용됩니다. 즉 a에 이미 값이 있으면 오른쪽은 아예 평가되지 않죠. 옵션 객체에서 빠진 값만 기본값으로 채울 때 특히 유용합니다.