← Back to Posts
javascripttypescript

optional chaining은 만병통치약?

optional chaining은 만병통치약일까?

2025년 11월 09일

목차

  • 우리는 optional chaining을 주로 언제 사용하는가
  • optional chaining의 내부 동작
  • 기본 개념
  • Babel의 트랜스파일 예시
  • optional chaining을 남용했을 때의 문제점
  • 가독성을 해친다
  • 오류를 찾기 어렵게 한다
  • optional chaining 대신 사용할 수 있는 패턴
  • AND(&&) 연산자 사용
  • Proxy 객체 활용
  • 사용자 정의 헬퍼 함수
  • zod를 사용한
  • optional chaining을 남용하는 휴먼 에러를 방지할 수 있는 방법
  • ESLint rule 설정
  • 참고 자료

우리는 optional chaining을 주로 언제 사용하는가

TypeScript로 개발을 하다보면 서버로부터 가져온 객체의 속성에 접근하면서 optional chaining을 별 생각 없이 남발(?)하게 되는 경우가 많았던 것 같습니다. 주로 아래와 같이 말이죠.

const { data: products } = useProducts(id);

return (
	<div>
		{products?.map((product) => (
			<div key={product?.id}>
				...
			</div>
		}
	</div>
)

위와 같이 optional chaining을 객체의 속성에 접근할 때마다 사용해주면 TypeScript에서 종종 뱉는 불만(_Uncaught TypeError: Cannot read property ‘foo’ of undefined)_이 사라집니다.

optional chaining의 내부 동작

기본 개념

💡 왼쪽 피연산자(obj)가 null 또는 undefined이면 TypeError를 던지지 않고, 전체 표현식 결과를 undefined로 반환한다.

optional chaining의 개념적 의미는 위와 같습니다. 즉,

a?.b

는 내부적으로

(a == null) ? undefined : a.b

와 동일한 의미를 가집니다.

Babel의 트랜스파일 예시

Babel은 호환성을 위해 optional chaining을 다음처럼 변환합니다.

const result = data?.methods?.fetch?.(results?.payload?.options)?.result ?? [];

트랜스파일 후:

var _data, _data_methods, _data_methods_fetch, _results_payload, _data_methods_fetch_call;
const result =
  (_data = data) == null ? void 0 :
  (_data_methods = _data.methods) == null ? void 0 :
  (_data_methods_fetch = _data_methods.fetch) == null ? void 0 :
  (_data_methods_fetch_call = _data_methods_fetch((_results_payload = results.payload) == null ? void 0 : _results_payload.options)) == null ? void 0 :
  _data_methods_fetch_call.result ?? [];

보면 각 체이닝 포인트마다 nullish 검사와 임시 변수 할당이 추가되어 있습니다. 이 때문에 번들 사이즈가 커지고 성능이 저하될 수 있습니다.

optional chaining을 남용했을 때의 문제점

가독성을 해친다

오류를 찾기 어렵게 한다

  • optional chaining은 실패를 침묵시키기 때문에 문제 원인을 찾기 어렵습니다.

optional chaining 대신 사용할 수 있는 패턴

AND(&&) 연산자 사용

if (foo && foo.bar && foo.bar.baz) {
  foo.bar.baz();
}

Proxy 객체 활용

ES6 Proxy를 이용해 안전하게 값을 반환하는 패턴도 있습니다. Proxy를 활용하면 undefined 속성이 접근될 경우 커스텀 동작(예: 특별한 객체 반환 등)이 가능합니다.

function safe(obj) {
  return new Proxy(obj, {
    get: function(target, name){
      return name in target ? (typeof target[name] === 'object' ? safe(target[name]) : target[name]) : undefined;
    }
  });
}
Using ES6's Proxy for safe Object property access
How to use the Javascript ES6 Proxy object to create a safe wrapper around an object in order to prevent an undefined exception
gidi.io
Using ES6's Proxy for safe Object property access

사용자 정의 헬퍼 함수

const pick = (target, path) => path.split(".").reduce((acc, key) => acc && acc[key], target);
const age = pick(data, 'student.age');
get | Slash libraries
객체의 특정 경로에 있는 값을 반환합니다.
www.slash.page

zod를 사용한 스키마 검증 & 정규화 (API 경계에서의 검증)

import { z } from "zod";

export const TabDto = z.object({
  // 서버가 최소한 제공한다 가정하는 필드들 (실제 키 이름에 맞게 수정)
  id: z.string().or(z.number()).transform(String),
  label: z.string().trim().min(1).optional(),
  slug: z.string().trim().min(1).optional(),     // routeName 후보
  icon: z.string().optional(),
});

export const TabsResponseDto = z.array(TabDto);

// 앱 내부에서 쓸 도메인 모델: name/label은 항상 존재
export type Tab = {
  id: string;
  routeName: string;   // React Navigation screen name (고유)
  label: string;       // 탭에 보여줄 글자
  icon?: string;
};
// libs/normalizeTabs.ts
import slugify from "slugify";
import { TabsResponseDto, Tab } from "./schemas";
import { uniqBy } from "lodash";

type NormalizeResult =
  | { kind: "ok"; tabs: Tab[]; issues: string[] }
  | { kind: "empty"; tabs: Tab[]; issues: string[] }; // 폴백 발생

const toSlug = (s: string) =>
  slugify(s, { lower: true, strict: true }) || "tab";

export function normalizeTabs(input: unknown): NormalizeResult {
  const issues: string[] = [];

  const parsed = TabsResponseDto.safeParse(input);
  if (!parsed.success) {
    issues.push("tabs: invalid shape");
    return { kind: "empty", tabs: defaultTabs(), issues };
  }
  const raw = parsed.data;

  // 필수 정보 정규화: label/slug 비어있으면 대체
  let mapped: Tab[] = raw.map((t, i) => {
    const base = t.slug ?? t.label ?? `tab-${i + 1}`;
    const routeName = toSlug(base);
    const label = t.label?.trim() || `Tab ${i + 1}`;
    return { id: t.id, routeName, label, icon: t.icon };
  });

  // 중복 routeName 처리
  const seen = new Map<string, number>();
  mapped = mapped.map((t) => {
    const count = (seen.get(t.routeName) ?? 0) + 1;
    seen.set(t.routeName, count);
    return count === 1 ? t : { ...t, routeName: `${t.routeName}-${count}` };
  });

  // 완전 중복(같은 id, 같은 routeName) 제거 (보수적)
  mapped = uniqBy(mapped, (t) => `${t.id}:${t.routeName}`);

  // 결과가 비거나 모두 “의미 없는” 값이면 폴백
  if (mapped.length === 0) {
    issues.push("tabs: empty after normalization");
    return { kind: "empty", tabs: defaultTabs(), issues };
  }

  return { kind: "ok", tabs: mapped, issues };
}

function defaultTabs(): Tab[] {
  return [
    { id: "home", routeName: "home", label: "Home" },
    { id: "feed", routeName: "feed", label: "Feed" },
  ];
}

optional chaining을 남용하는 휴먼 에러를 방지할 수 있는 방법

ESLint rule 설정

  1. no-unsafe-optional-chaining

    no-unsafe-optional-chaining - ESLint - Pluggable JavaScript Linter
    A pluggable and configurable linter tool for identifying and reporting on patterns in JavaScript. Maintain your code quality with ease.
    eslint.org
    no-unsafe-optional-chaining - ESLint - Pluggable JavaScript Linter
  2. unicorn/explicit-length-check

    eslint-plugin-unicorn/docs/rules/explicit-length-check.md at 8fcae1aa5a3f1f2c6014bf33aeda2f016c0a8d9a · sindresorhus/eslint-plugin-unicorn
    More than 100 powerful ESLint rules. Contribute to sindresorhus/eslint-plugin-unicorn development by creating an account on GitHub.
    github.com
    eslint-plugin-unicorn/docs/rules/explicit-length-check.md at 8fcae1aa5a3f1f2c6014bf33aeda2f016c0a8d9a · sindresorhus/eslint-plugin-unicorn
  3. @typescript-eslint/no-unnecessary-condition

    no-unnecessary-condition | typescript-eslint
    Disallow conditionals where the type is always truthy or always falsy.
    typescript-eslint.io
    no-unnecessary-condition | typescript-eslint

참고 자료

Optional chaining - JavaScript | MDNMDN
optional chaining 연산자 (?.) 는 체인의 각 참조가 유효한지 명시적으로 검증하지 않고, 연결된 객체 체인 내에 깊숙이 위치한 속성 값을 읽을 수 있다.
developer.mozilla.org
Optional chaining '?.'
javascript.info
Optional chaining '?.'
The Dangers of Optional Chaining Overuse
Optional chaining is one of my favorite javascript operators (if such a thing exists), but it's best to avoid using it when not necessary.
keegan.codes
The Dangers of Optional Chaining Overuse
Cost of unnecessary Optional Chaining (and Nullish coalescing operator)
If you are a developer who has worked with JavaScript in recent years, I am sure you are well aware...
dev.to
Cost of unnecessary Optional Chaining (and Nullish coalescing operator)
Just a moment...
blog.mrinalmaheshwari.com