React TypeScript optional props 기본값 처리: undefined·null 차이

2026.06.11·수정 2026.09.12·약 24분·작성: 해비·블로그 소개

목표·선수 지식: props와 구조 분해를 바탕으로 생략과 null의 기본값을 실행 결과로 구분합니다. 본문의 짧은 문법 블록은 서로 독립된 부분 예제입니다. 같은 파일에 모두 합치면 중복 선언이 생길 수 있습니다.

optional props와 기본값 핵심 차이

prop?: Type은 prop을 생략할 수 있다는 타입 표현이고, 값을 자동으로 넣지는 않습니다. 실제 기본값은 구조 분해의 prop = value 또는 prop ?? value로 처리합니다. 구조 분해 기본값은 undefined에만 적용되고, ??undefinednull을 모두 처리합니다.

실습 경로: 현재 ZIP전체 코드. 먼저 설명에서 지정한 파일만 읽고, 설정·테스트 파일은 필요한 때 확인하세요.

작성 방식 의미 null 처리
size?: "sm" | "md" prop 생략 허용 타입에 null을 넣지 않으면 허용 안 됨
size = "md" undefined일 때 기본값 null은 그대로 유지
size ?? "md" undefined 또는 null일 때 기본값 null도 기본값으로 변경
onClose?.() 함수가 있을 때만 호출 없으면 실행하지 않음

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

props 타입의 기본 구조부터 확인하려면 React 컴포넌트 props 타입 지정하기에서 부모와 자식 사이의 값 구조를 함께 볼 수 있습니다.

두 개념은 같이 자주 나오지만 같은 기능은 아닙니다. 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는 생략 가능하게 열어두었습니다. 사용하는 쪽에서는 아래처럼 쓸 수 있습니다.

children 타입 자체가 헷갈린다면 TypeScript children 타입에서 ReactNodePropsWithChildren의 차이를 확인할 수 있습니다.

<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만 기본값으로 넘기는 기준으로 이해하면 됩니다.

직접 확인하는 경계와 재현 방법

이 검사는 exactOptionalPropertyTypes: true에서 실행합니다. strict만으로 이 옵션이 켜지지는 않습니다. 속성 생략은 blue, null도 blue, gray는 gray로 렌더링되어야 합니다. 명시적 undefined를 API로 허용하려면 color?: “blue” | “gray” | null | undefined로 선언하세요. 타입 테스트가 그 변경을 감지합니다.

아래는 확인 실습의 전체 파일입니다. 빈 실습 폴더의 루트를 기준으로 표시된 경로에 모두 저장하거나 같은 내용의 ZIP을 내려받으세요.

현재 실습 ZIP

현재 본문과 같은 실습 파일 내려받기

check.tsx

import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
type Props = { color?: "blue" | "gray" | null };
function Badge({ color }: Props) {
  return <span className={`badge-${color ?? "blue"}`}>상태</span>;
}
for (const props of [
  {},
  { color: null },
  { color: "gray" },
] satisfies Props[]) {
  console.log(renderToStaticMarkup(createElement(Badge, props)));
}
function typeChecks() {
  // @ts-expect-error exactOptionalPropertyTypes: true에서는 명시적 undefined를 별도로 허용해야 합니다.
  const invalid: Props = { color: undefined };
  void invalid;
}
void typeChecks;

tsconfig.json

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "CommonJS",
    "outDir": "dist",
    "noEmitOnError": true,
    "types": [],
    "jsx": "react-jsx",
    "esModuleInterop": true,
    "exactOptionalPropertyTypes": true
  },
  "files": ["check.tsx"]
}

package.json

{
  "private": true,
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "start": "node dist/check.js"
  },
  "devDependencies": {
    "typescript": "7.0.2",
    "@types/react": "19.3.0",
    "@types/react-dom": "19.3.0"
  },
  "dependencies": {
    "react": "19.3.0",
    "react-dom": "19.3.0"
  }
}
npm install
npm run build
npm start

실제 확인 결과:

<span class="badge-blue">상태</span>
<span class="badge-blue">상태</span>
<span class="badge-gray">상태</span>

검증 환경은 Node.js v24.19.0, TypeScript 7.0.2, React·React DOM 및 해당 타입 19.3.0입니다. 설치되어 있던 의존성으로 타입 검사와 위 실행을 확인했습니다. 새 환경의 npm install, 실제 서버 응답, 브라우저 클릭·모바일 화면은 이 검증에 포함하지 않습니다.

공식 출처 확인일: 2026-09-12. exactOptionalPropertyTypes.html · react-19-upgrade-guide

. 완성형 뷰어와 같은 코드이며 README와 검증 기록을 포함합니다.

이 글이 도움이 되었나요?

조회 중

TypeScript 학습 순서

필수 11개 · 전체 11개

읽음 기록 관리

전체 과정 목차 (11개)
  1. 필수 학습 · TypeScript란 무엇인가: JavaScript와 차이 정리
  2. 필수 학습 · TypeScript 기본 타입: 객체와 함수까지 strict로 익히기
  3. 필수 학습 · TypeScript 타입 좁히기: union null unknown 안전 검증
  4. 필수 길잡이 · TypeScript 실무 로드맵
  5. 필수 학습 · TypeScript as const 사용법: union 타입을 자동 생성하는 기준
  6. 필수 학습 · React TypeScript 기본 구조: tsx 파일과 props 타입 기준
  7. 필수 학습 · React TypeScript optional props 기본값 처리: undefined·null 차이 현재 글
  8. 필수 학습 · TypeScript children 타입: ReactNode와 PropsWithChildren 차이
  9. 필수 학습 · TypeScript 제네릭 쉽게 이해하기: props와 API 응답 타입으로 감 잡기
  10. 필수 학습 · TypeScript 고급 상태 모델링: 판별 유니온과 never 타입 테스트
  11. 필수 학습 · TypeScript satisfies 사용법: 타입 지정·as const와 비교하기

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기