본문으로 건너뛰기
기술스택 · stack/javascript/옵셔널 체이닝: 없을 수도 있는 값 안전하게 읽기

JavaScript · 4장 객체

옵셔널 체이닝: 없을 수도 있는 값 안전하게 읽기

서버에서 받은 데이터에는 빠진 값이 자주 있어요. 회원에게 주소가 없거나, 글 목록이 비어 있거나, 응답 자체가 null이기도 해요. 이럴 때 점(.)으로 깊이 읽어 들어가면 화면 전체가 오류로 멈춰요. ?.는 "있으면 읽고, 없으면 거기서 멈춰"라고 말하는 문법이에요.

#4.6

01. 없는 값의 속성을 읽으면

없는 속성을 읽으면 undefined가 나와요. 여기까지는 괜찮아요. 문제는 그 undefined에서 한 번 더 속성을 읽을 때예요. undefined와 null은 속성을 가질 수 없어서 TypeError가 나요.

const user = { name: '하늘' }; // 주소를 아직 등록하지 않은 회원

console.log(user.address);       // undefined — 여기까지는 괜찮아요
console.log(user.address.city);  // undefined에서 city를 읽으려 해요

예전에는 user.address && user.address.city처럼 앞쪽이 있는지 하나씩 확인했어요(단락 평가). 단계가 깊어지면 같은 이름을 몇 번씩 되풀이해야 해서 읽기 힘들어요. 이걸 줄여 주는 게 옵셔널 체이닝 ?.예요.

02. ?.의 세 가지 모양과 멈추는 규칙

a?.b는 a가 null이나 undefined면 더 읽지 않고 undefined를 돌려줘요. a가 그 밖의 값이면 a.b와 똑같아요. 쓰는 곳에 따라 세 가지 모양이 있어요.

  • obj?.prop — 속성 읽기
  • obj?.[key] — 대괄호로 읽기 (키가 변수에 있을 때)
  • fn?.() — 함수가 있을 때만 부르기
const user = { name: '하늘' };
const key = 'name';

console.log(user.address?.city); // undefined — 오류 대신 멈춰요
console.log(user?.[key]);        // 하늘

let calls = 0;
const list = null;
console.log(list?.find(calls++)); // undefined
console.log(calls);               // 0 — 뒤쪽은 아예 실행하지 않았어요

마지막 예처럼 ?. 앞이 비어 있으면 뒤에 이어진 나머지 전체를 건너뛰어요. 함수 호출도, 인자 계산(calls++)도 하지 않아요. 그래서 a?.b.c.d처럼 ?.를 한 번만 써도, a가 비어 있으면 .c, .d에서 오류가 나지 않아요.

①같은 식 res.data?.profile?.nickname ?? '이름 없음'을 응답 세 개에 써 봐요. A는 끝까지 읽어서 '하늘'이에요. B는 profile이 없어서 ?.nickname에서, C는 data가 null이라서 ?.profile에서 멈추고 undefined가 돼요. 그러면 ??가 기본값 '이름 없음'을 넣어요.

03. API 응답 다루기: ?.와 ??를 함께

실무에서 ?.는 거의 늘 ??와 짝을 지어 써요. ?.가 멈추면 undefined가 나오고, ??는 null · undefined일 때만 기본값을 넣어 주니까요(?? 연산자). 배열의 첫 항목은 ?.[0], 개수는 ?.length ?? 0으로 읽어요.

const responses = [
  { data: { profile: { nickname: '하늘', posts: [{ title: '첫 글' }] } } },
  { data: { profile: { nickname: '가람' } } }, // 글이 없어요
  { data: null },                              // 탈퇴한 회원
];

for (const res of responses) {
  const name = res.data?.profile?.nickname ?? '이름 없음';
  const first = res.data?.profile?.posts?.[0]?.title ?? '글 없음';
  const count = res.data?.profile?.posts?.length ?? 0;
  console.log(`${name} · ${first} · ${count}편`);
}

기본값은 ||가 아니라 ??로 붙여요. 글이 0편인 것은 "값이 없는 것"이 아니라 "0이라는 값"이에요. posts?.length || '정보 없음'이라고 쓰면 0도 거짓 같은 값이라 '정보 없음'으로 바뀌어 버려요.

?.()는 넘겨받을 수도, 안 받을 수도 있는 콜백을 부를 때 편해요. if (options.onDone) options.onDone()을 한 줄로 줄여 줘요.

function save(memo, options = {}) {
  console.log('저장:', memo);
  options.onDone?.('완료'); // 콜백을 받았을 때만 불러요
}

save('메모 1');
save('메모 2', { onDone: msg => console.log('알림:', msg) });

04. ?.를 남용하면 버그가 숨어요

?.는 오류를 undefined로 바꿔 줘요. 이 말은 있어야 할 값이 없을 때도 조용히 넘어간다는 뜻이에요. 아래처럼 속성 이름에 오타가 나도 ?.는 아무 말을 하지 않고, 화면에는 빈칸이나 undefined가 찍혀요.

const order = { id: 7, customer: { name: '하늘' } }; // 주문에는 늘 customer가 있어요

console.log(order?.custmer?.name); // undefined — 오타인데 조용히 넘어가요
console.log(order.custmer.name);   // 오류가 나서 바로 알 수 있어요

그래서 기준은 하나예요. 없을 수도 있다고 정해진 값에만 ?.를 써요. API 문서에 "선택"이라고 적힌 필드, 아직 불러오기 전인 데이터, 넘겨도 되고 안 넘겨도 되는 옵션 같은 것이에요. 반드시 있어야 하는 값은 점으로 읽어서, 빠졌을 때 오류가 바로 나게 두는 편이 버그를 빨리 찾아요.

그 밖에 알아 둘 규칙 세 가지예요.

user?.name = '하늘'; // SyntaxError — ?.는 읽을 때만 써요. 대입은 안 돼요
delete user?.name;   // 이건 돼요 — user가 있으면 지워요
notDeclared?.x;      // ReferenceError — 선언하지 않은 변수까지 막아 주지는 않아요

주문 목록을 한 줄씩 요약하는 함수인데, 두 번째 주문에서 오류가 나요. 주문의 id는 늘 있지만 회원 정보 · 상품 목록 · 배송 주소는 없을 수도 있어요. 아래처럼 나오게 고쳐 보세요.

#1 하늘 · 2개 · 서울
#2 비회원 · 1개 · 주소 없음
#3 가람 · 0개 · 주소 없음
const orders = [
  { id: 1, customer: { name: '하늘' }, items: ['연필', '공책'], shipping: { address: { city: '서울' } } },
  { id: 2, items: ['지우개'], shipping: { address: null } },
  { id: 3, customer: { name: '가람' } },
];

function summary(order) {
  return `#${order.id} ${order.customer.name} · ${order.items.length}개 · ${order.shipping.address.city}`;
}

for (const order of orders) console.log(summary(order));
해답 보기

없을 수도 있는 값 바로 뒤에 ?.를 쓰고, 멈췄을 때 넣을 기본값을 ??로 정해요. order와 order.id는 늘 있으니 ?.를 붙이지 않아요. 붙이면 주문 자체가 빠진 버그를 숨기게 돼요.

function summary(order) {
  const name = order.customer?.name ?? '비회원';
  const count = order.items?.length ?? 0;
  const city = order.shipping?.address?.city ?? '주소 없음';
  return `#${order.id} ${name} · ${count}개 · ${city}`;
}

05. 실전 포인트

  • 언제 쓰나요? API 응답의 선택 필드, 불러오기 전이라 아직 null인 데이터, 넘기지 않아도 되는 옵션과 콜백(options.onDone?.())을 읽을 때 써요. 기본값은 ??로 붙여요.
  • 대신 쓰는 것: 반드시 있어야 하는 값은 점으로 읽어서 빠지면 바로 오류가 나게 둬요. 응답 모양 자체를 믿을 수 없다면 받는 곳에서 한 번 검사하고 기본값을 채운 뒤, 나머지 코드는 점으로 읽는 편이 깔끔해요.
  • 흔한 실수: 모든 점을 ?.로 바꿔 오타와 누락을 숨기는 것, ?? 대신 ||로 기본값을 붙여 0이나 빈 문자열을 잃는 것, obj?.a = 1처럼 대입에 쓰려는 것이에요.

정리

  • a?.b는 a가 null · undefined면 멈추고 undefined를 돌려줘요.
  • 모양은 ?.prop · ?.[key] · ?.() 세 가지이고, 멈추면 뒤쪽 전체를 건너뛰어요.
  • API 응답은 ?.로 읽고 ??로 기본값을 정해요.
  • 없을 수도 있는 값에만 써요. 남용하면 오타와 누락이 조용히 숨어요.
const res = { data: null };일 때 res.data?.items.length는 오류가 날까요?

오류가 나지 않고 undefined예요. res.data가 null이라 ?.에서 멈추고, 뒤에 이어진 .items.length 전체를 건너뛰어요.

글이 0편일 때 posts?.length || '정보 없음'은 무엇을 보여 줄까요?

'정보 없음'이에요. 0도 거짓 같은 값이라서 ||가 기본값으로 바꿔 버려요. posts?.length ?? '정보 없음'으로 쓰면 null · undefined일 때만 바꿔서 0이 그대로 나와요.

반드시 있어야 하는 값에는 ?.를 쓰지 않는 게 좋은 이유는?

?.는 값이 없을 때 오류 대신 undefined를 돌려줘서, 오타나 데이터 누락 같은 버그를 숨겨요. 있어야 할 값은 점으로 읽어 빠지면 바로 오류가 나게 두는 편이 원인을 빨리 찾아요.

SIMPLE · 기술스택 / JavaScript — 4장 객체