타입스크립트

TypeScript optional props와 default value 처리하기

2026.06.11·약 18분

핵심 정리

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

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은 필수입니다. 버튼 안에 들어갈 내용이 없으면 컴포넌트 자체가 성립하기 어렵기 때문입니다. 반면 variantsize는 생략 가능하게 열어두었습니다. 사용하는 쪽에서는 아래처럼 쓸 수 있습니다.

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

문제는 여기서 끝나지 않습니다. variantsize를 생략할 수 있게 만들었기 때문에 컴포넌트 내부에서 두 값은 없을 수도 있습니다. 즉 TypeScript 입장에서는 variant"primary""secondary"일 수도 있지만, 아무 값도 없을 수도 있다고 판단합니다.

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

tsconfig에서 exactOptionalPropertyTypes를 켜면 이 차이는 더 분명해집니다. 예를 들어 variant?: "primary" | "secondary"는 “속성이 없을 수 있다”는 뜻에 더 가까워지고, variantundefined를 명시적으로 대입하는 것까지 자연스럽게 허용한다는 의미는 아닙니다. 초반 학습 단계에서는 옵션 이름까지 외울 필요는 없지만, optional property와 | 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도 배열로 조립하면 optional boolean 값 때문에 빈 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;
  onClose?: () => void;
};

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

onClose?.()onClose가 있을 때만 실행합니다. 닫기 버튼 자체는 있지만 부모에서 닫기 동작을 넘기지 않는 구조라면 이런 방식이 필요합니다. 다만 모달에서 닫기 기능이 반드시 있어야 한다면 onClose를 optional로 만들지 않는 것이 더 적절합니다.

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 기본 매개변수나 구조 분해 기본값으로 처리하는 방식이 맞습니다. 기존 프로젝트의 오래된 컴포넌트를 읽을 때는 defaultProps를 만날 수 있지만, 새 컴포넌트를 작성할 때는 JavaScript 기본값 문법으로 정리하는 쪽이 일관됩니다.

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

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

댓글 남기기