TypeScript optional props와 default value 처리법

2026.06.11·수정 2026.07.19·약 18분

핵심 정리

optional props는 props에 기본값을 넣는 문법이 아니라, 해당 prop을 넘기지 않아도 컴포넌트를 사용할 수 있게 만드는 타입 표현입니다. 기본값은 TypeScript 타입 선언만으로 처리되지 않고, 실행 코드에서 별도로 정리해야 합니다.

React 컴포넌트에 TypeScript를 붙이기 시작하면 처음에는 모든 props에 타입을 붙이는 것만으로도 충분해 보입니다. 그런데 컴포넌트를 조금만 재사용하다 보면 바로 다음 문제가 나옵니다. 어떤 prop은 반드시 받아야 하고, 어떤 prop은 없어도 기본 UI로 처리할 수 있습니다. 이때 사용하는 문법이 optional props이고, 함께 고민해야 하는 것이 default value입니다.

두 개념은 같이 자주 나오지만 같은 기능은 아닙니다. variant?: "primary" | "secondary"variant를 생략할 수 있다는 뜻입니다. 반면 variant = "primary"는 실제 컴포넌트가 실행될 때 값이 없으면 "primary"를 쓰겠다는 뜻입니다. 타입 단계와 실행 단계를 구분하지 않으면 props 설계가 금방 흔들립니다.

optional props는 기본값을 넣는 문법이 아니다

TypeScript optional props에서 물음표와 undefined의 관계를 설명하는 다이어그램

TypeScript에서 객체 타입의 property 뒤에 ?를 붙이면 그 property는 optional property가 됩니다. React props도 결국 객체이기 때문에 같은 방식으로 생략 가능한 prop을 표현합니다.

type ButtonProps = {
  children: React.ReactNode;
  variant?: "primary" | "secondary";
  size?: "sm" | "md";
};

function Button({ children, variant, size }: ButtonProps) {
  return (
    <button className={`button button-${variant} button-${size}`}>
      {children}
    </button>
  );
}

위 코드에서 children prop은 필수입니다. 다만 React.ReactNode에는 null, undefined, false도 포함될 수 있으므로, 렌더링 가능한 내용이 항상 있다는 뜻은 아닙니다. 반면 variantsize는 생략 가능하게 열어두었습니다. 사용하는 쪽에서는 아래처럼 쓸 수 있습니다.

<Button>저장하기</Button>
<Button variant="secondary">취소</Button>
<Button size="sm">작게 보기</Button>

문제는 여기서 끝나지 않습니다. variantsize를 생략할 수 있게 만들었기 때문에 컴포넌트 내부에서 두 값은 없을 수도 있습니다. 즉 TypeScript 입장에서는 variant"primary""secondary"일 수도 있지만, 아무 값도 없을 수도 있다고 판단합니다. 이 상태로 className을 만들면 button-undefined 같은 값이 생길 수 있으므로, optional로 열어둔 prop은 컴포넌트 안에서 기본값을 정리해야 합니다.

variant?: "primary" | "secondary"라고 썼다고 해서 variant에 자동으로 "primary"가 들어가지는 않습니다. optional은 허용 범위를 넓히는 타입 표현이고, 기본값은 별도로 작성해야 하는 실행 코드입니다.

tsconfig에서 exactOptionalPropertyTypes를 켜면 이 차이는 더 분명해집니다. 예를 들어 variant?: "primary" | "secondary"는 “속성이 없을 수 있다”는 뜻에 더 가까워지고, variantundefined를 명시적으로 대입하는 것까지 자연스럽게 허용한다는 의미는 아닙니다. 다만 값을 읽을 때는 속성이 없을 수 있으므로 여전히 "primary" | "secondary" | undefined처럼 다뤄야 합니다.

React 컴포넌트에서 optional props가 필요한 순간

optional props는 사용 편의를 위해 필요합니다. 모든 props를 필수로 만들면 컴포넌트를 사용할 때마다 같은 값을 반복해서 넘겨야 합니다. 버튼의 기본 스타일이 대부분 primary라면 매번 variant="primary"를 쓰게 만들 이유가 없습니다.

type ButtonProps = {
  children: React.ReactNode;
  variant?: "primary" | "secondary";
  size?: "sm" | "md";
  disabled?: boolean;
  fullWidth?: boolean;
};

이런 prop들은 optional로 두기 좋습니다. 없어도 핵심 동작이나 표시 기준이 불명확해지지 않고, 기본 UI 규칙을 적용할 수 있는 값이기 때문입니다. variant가 없으면 기본 버튼, size가 없으면 보통 크기, disabled가 없으면 활성 상태로 처리할 수 있습니다.

반대로 화면에 반드시 필요한 값은 optional로 만들면 안 됩니다. 상품 카드의 상품명, 상세 페이지의 게시글 id, 입력 컴포넌트의 접근성 label처럼 없으면 기능이나 의미가 깨지는 값은 필수 prop으로 두는 편이 좋습니다.

type ProductCardProps = {
  id: string;
  name: string;
  price: number;
  imageUrl?: string;
};

위 예시에서 id, name, price는 필수입니다. 카드가 어떤 상품을 나타내는지 결정하는 핵심 데이터이기 때문입니다. imageUrl은 optional로 둘 수 있습니다. 이미지가 없을 때 기본 썸네일이나 텍스트형 fallback을 보여줄 수 있기 때문입니다.

optional props를 판단할 때는 “없어도 컴포넌트가 정상적인 의미를 유지하는가?”를 먼저 보면 됩니다. 단순히 사용하기 편하다는 이유로 모든 prop에 ?를 붙이면, 사용하는 쪽의 코드는 편해질 수 있지만 컴포넌트 내부는 계속 undefined를 방어해야 합니다.

default value는 컴포넌트 내부에서 처리한다

React 컴포넌트에서 구조 분해 기본값과 null 처리 차이를 비교하는 체크리스트 이미지

function component에서는 props를 구조 분해하면서 기본값을 주는 방식이 가장 자주 쓰입니다. 기본값이 명확한 UI 옵션이라면 이 방식이 깔끔합니다.

import type { ReactNode } from "react";

type ButtonProps = {
  children: ReactNode;
  variant?: "primary" | "secondary";
  size?: "sm" | "md";
  disabled?: boolean;
  fullWidth?: boolean;
};

export function Button({
  children,
  variant = "primary",
  size = "md",
  disabled = false,
  fullWidth = false,
}: ButtonProps) {
  return (
    <button
      type="button"
      disabled={disabled}
      className={[
        "button",
        `button-${variant}`,
        `button-${size}`,
        fullWidth ? "is-full" : null,
      ]
        .filter(Boolean)
        .join(" ")}
    >
      {children}
    </button>
  );
}

이렇게 작성하면 컴포넌트 본문 안에서는 variantsize를 확정된 값처럼 다룰 수 있습니다. 사용하는 쪽에서 값을 넘기지 않아도 variant"primary", size"md"로 정리됩니다. className도 배열로 조립하면 조건부 class 값을 깔끔하게 제외할 수 있습니다.

다만 기본값이 항상 구조 분해 위치에 있어야 하는 것은 아닙니다. 값이 없을 때 보여줄 문구를 렌더링 직전에 정하고 싶다면 ??를 사용하는 방식이 더 자연스러울 수 있습니다.

type ProfileCardProps = {
  name: string;
  description?: string;
  imageUrl?: string | null;
};

function ProfileCard({ name, description, imageUrl }: ProfileCardProps) {
  return (
    <article className="profile-card">
      {imageUrl ? (
        <img src={imageUrl} alt={`${name} 프로필 이미지`} />
      ) : (
        <div className="profile-card__placeholder">No Image</div>
      )}

      <h3>{name}</h3>
      <p>{description ?? "소개가 아직 등록되지 않았습니다."}</p>
    </article>
  );
}

여기서 description은 없을 때 안내 문구로 대체됩니다. ??는 왼쪽 값이 null 또는 undefined일 때만 오른쪽 값을 사용합니다. 빈 문자열까지 기본 문구로 바꾸고 싶다면 별도 조건이 필요합니다. 빈 문자열은 “값이 없음”일 수도 있지만, 사용자가 의도적으로 비워둔 값일 수도 있기 때문입니다.

undefined와 null은 같은 값으로 보면 안 된다

optional props를 다룰 때 자주 놓치는 부분이 null입니다. 구조 분해 기본값은 값이 undefined일 때 적용됩니다. 부모 컴포넌트가 prop을 아예 넘기지 않았거나, 값이 undefined라면 기본값이 들어갑니다. 하지만 null을 명시적으로 넘기면 기본값이 적용되지 않습니다.

type BadgeProps = {
  color?: "blue" | "gray" | null;
};

function Badge({ color = "blue" }: BadgeProps) {
  return <span className={`badge badge-${color}`}>상태</span>;
}

이 컴포넌트에서 <Badge />처럼 사용하면 color"blue"가 됩니다. 하지만 <Badge color={null} />처럼 사용하면 color는 그대로 null입니다. 결과적으로 badge-null 같은 의도하지 않은 className이 만들어질 수 있습니다.

그래서 null까지 들어올 수 있는 prop이라면 기본값을 구조 분해에만 맡기지 말고, 컴포넌트 내부에서 명시적으로 정리하는 편이 낫습니다.

type BadgeProps = {
  color?: "blue" | "gray" | null;
};

function Badge({ color }: BadgeProps) {
  const badgeColor = color ?? "blue";

  return <span className={`badge badge-${badgeColor}`}>상태</span>;
}

이 방식은 undefinednull을 모두 기본값으로 대체합니다. API 응답에서 null이 자주 내려오는 프로젝트라면 이 차이를 알고 있어야 합니다. TypeScript의 optional은 “prop이 없을 수 있음”을 표현하는 것이고, null은 “값이 비어 있음을 명시적으로 전달함”에 가깝습니다.

optional props를 잘못 쓰면 생기는 문제

optional props에서 가장 흔한 실수는 모든 값을 optional로 열어두는 것입니다. 처음에는 컴포넌트 사용이 편해 보이지만, 필수 데이터가 빠져도 타입 오류가 나지 않기 때문에 문제를 늦게 발견합니다.

type UserListItemProps = {
  id?: string;
  name?: string;
  email?: string;
};

사용자 목록 아이템에서 id, name, email이 모두 optional이면 컴포넌트는 어떤 데이터를 기준으로 렌더링해야 하는지 불명확해집니다. 리스트 key, 클릭 이동, 화면 표시가 모두 흔들릴 수 있습니다. 이 경우에는 최소한 화면과 동작에 필요한 값은 필수로 두는 것이 맞습니다.

type UserListItemProps = {
  id: string;
  name: string;
  email?: string;
};

두 번째 실수는 optional callback prop을 바로 호출하는 것입니다. 부모가 넘기지 않을 수도 있는 함수라면 호출 전에 존재 여부를 확인해야 합니다. 이때 핵심은 “있으면 실행한다”와 “반드시 있어야 한다”를 타입에서 먼저 구분하는 것입니다.

type ModalProps = {
  title: string;
  onAfterClose?: () => void;
};

function Modal({ title, onAfterClose }: ModalProps) {
  return (
    <section className="modal">
      <h2>{title}</h2>
      <button type="button" onClick={() => onAfterClose?.()}>
        닫기
      </button>
    </section>
  );
}

onAfterClose?.()onAfterClose가 있을 때만 실행합니다. 다만 닫기 버튼이 실제로 모달을 닫아야 하는 구조라면 닫기 동작 자체를 optional로 두지 않는 편이 맞습니다. optional callback은 onAfterCloseonReset처럼 있어도 되고 없어도 되는 부가 동작에 더 잘 맞습니다.

boolean prop도 주의해야 합니다. disabled?: boolean처럼 작성하면 생략 시 값은 undefined입니다. React에서 disabled={undefined}는 결과적으로 비활성화되지 않은 상태로 동작하므로 보통 문제는 없지만, 컴포넌트 내부 조건식이 복잡해질 때는 기본값을 명확히 주는 편이 읽기 쉽습니다.

type ToggleProps = {
  checked?: boolean;
  disabled?: boolean;
};

function Toggle({ checked = false, disabled = false }: ToggleProps) {
  return (
    <button type="button" aria-pressed={checked} disabled={disabled}>
      {checked ? "켜짐" : "꺼짐"}
    </button>
  );
}

이렇게 기본값을 두면 checkeddisabled가 컴포넌트 안에서 어떤 기준으로 동작하는지 바로 보입니다. 특히 boolean prop은 이름만 보고도 기본 상태가 예상되어야 합니다. isOpen, disabled, fullWidth처럼 상태가 명확한 이름을 쓰고, 기본값은 컴포넌트 사용 패턴에 맞춰 정리하는 것이 좋습니다.

실무에서 정리해두면 좋은 판단 기준

props 타입을 설계할 때는 먼저 필수 값을 가려내는 편이 좋습니다. 컴포넌트가 정상적으로 의미를 가지려면 반드시 필요한 값, 이벤트 흐름상 반드시 필요한 함수, 접근성에 필요한 텍스트는 필수 prop으로 둡니다.

구분 처리 방식 예시
없으면 컴포넌트가 성립하지 않는 값 필수 prop id, name, children, label
없어도 기본 UI로 대체 가능한 값 optional prop + 기본값 variant, size, disabled
값이 없을 수 있음을 화면에 표현해야 하는 값 optional prop + 렌더링 분기 description, imageUrl, emptyText
있을 때만 실행하는 함수 optional callback + optional chaining onClose?.(), onReset?.()

기본값은 “디자인 시스템이나 컴포넌트 규칙상 가장 자연스러운 값”일 때 넣는 것이 좋습니다. 기본 버튼 스타일, 기본 크기, 기본 정렬처럼 팀 안에서 기준이 명확한 값은 구조 분해 기본값으로 처리하기 좋습니다. 반면 데이터가 없을 때 어떤 메시지를 보여줄지는 화면 맥락에 따라 달라질 수 있으므로 렌더링 위치에서 ??나 조건부 렌더링으로 처리하는 편이 더 읽기 쉽습니다.

또 하나 기억할 점은 defaultProps입니다. 예전 React 예제에서는 function component에도 Component.defaultProps를 붙이는 코드를 볼 수 있습니다. React 19에서는 function component의 defaultProps가 제거되었고, 기본값은 ES 기본 매개변수나 구조 분해 기본값으로 처리하는 방식이 맞습니다. class component의 defaultProps는 계속 지원되지만, 새 function component를 작성할 때는 JavaScript 기본값 문법으로 정리하는 쪽이 일관됩니다.

정리하면 optional props는 사용성을 높이기 위한 도구입니다. 하지만 타입을 느슨하게 만들기 위한 도구는 아닙니다. 없어도 되는 값만 optional로 만들고, 없을 때 컴포넌트가 어떤 기준으로 동작할지 default value나 렌더링 분기로 명확히 정리해야 합니다. 이 기준이 잡히면 다음 단계에서 이벤트 타입을 다룰 때도 onClick, onChange, onSubmit 같은 callback prop을 더 안정적으로 설계할 수 있습니다.

공식 기준을 함께 보면 차이가 더 분명합니다. TypeScript의 exactOptionalPropertyTypes 문서는 optional property와 명시적인 undefined 대입의 차이를 설명하고, React 19 업그레이드 가이드는 function component의 defaultProps 대신 ES 기본 매개변수를 쓰는 방향을 안내합니다. ?? 동작은 MDN nullish coalescing 문서처럼 nullundefined만 기본값으로 넘기는 기준으로 이해하면 됩니다.

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

댓글 남기기