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

··설명 약 8분·작성: 해비·블로그 소개

React TypeScript optional props에서 undefined와 null의 기본값 처리는 어떻게 다른가요?

생략 가능한 prop과 null은 같은 상태가 아닙니다. 구조 분해의 기본값은 값이 undefined일 때 적용되며, 명시적 null을 받으려면 타입에 허용하고 필요하면 ??로 대체합니다.

공식 문서 참고: React: prop 기본값과 undefined·null 차이 · TypeScript: 선택 속성 생략과 undefined 구분

직접 확인: 오류에서 수정까지

React TypeScript optional props 기본값 처리: undefined·null 차이 · 독립 실습

얻을 결과: label?: string에는 기본값을 적용하고 null을 허용할 때만 별도의 union을 선언합니다.

예제 크게 보기 (새 탭)

단독 검사는 tsconfig.json이 없는 새 폴더에서 example.ts 한 파일씩 교체하며 진행합니다. TypeScript가 없다면 설치 글에서 Node.js/npm·TypeScript 준비를 확인하세요. 타입 검사 성공은 코드 실행 성공과 구분합니다.

독자 질문: label을 생략한 경우와 null을 명시한 경우를 어떻게 구분할까요?

얻을 결과: label?: string에는 기본값을 적용하고 null을 허용할 때만 별도의 union을 선언합니다.

다음 코드를 별도 example.ts에 복사합니다.

type Props = { label?: string };
const props: Props = { label: null };

예상 오류 (TS2322): null은 string | undefined에 포함되지 않습니다. null을 허용하려면 label?: string | null로 명시하고 ?? 등으로 처리합니다.

오류 예제를 다음 코드로 교체합니다.

type Props = { label?: string };
function label({ label = '기본값' }: Props): string { return label; }
console.log(label({}));
npx tsc --strict --noEmit example.ts

기존 최소 예제 확인 기록: TypeScript 7.0.2의 별도 최소 예제에서 오류 TS2322, 수정 후 exit 0 확인. TaskFlow 전체 체크포인트는 미확인.

다음 학습: 20회차에서 children의 필수 여부와 ReactNode 타입을 정합니다.

optional prop

type TaskCardProps = {
  title: string;
  description?: string;
};

description은 전달되지 않을 수 있습니다.

<TaskCard
  title="TypeScript"
/>

컴포넌트 내부에서는 description이 string | undefined로 취급됩니다.

{description ? (
  <p>{description}</p>
) : null}

기본값

type ButtonProps = {
  label?: string;
};


function Button({
  label = '확인',
}: ButtonProps) {
  return (
    <button>
      {label}
    </button>
  );
}

undefined일 때만 기본값이 적용됩니다.

null은 다른 의미

type TaskDetailProps = {
  task:
    Task | null;
};

task prop 자체는 반드시 전달되지만 현재 선택된 Task가 없을 수 있습니다.

<TaskDetail
  task={null}
/>

이는 다음과 의미가 다릅니다.

task?: Task

optional은 property 자체가 빠질 수 있습니다.

undefined와 null 정책

프로젝트에서 규칙을 정하는 것이 좋습니다.

prop 자체가 생략 가능
→ optional / undefined


값은 전달되지만 현재 선택 없음
→ null

선택 상태에 null을 쓰면 prop 생략과 현재 선택 없음의 의미를 구분할 수 있습니다. 이 값의 의미는 해당 컴포넌트 계약에서 정합니다.

nullish coalescing

const label =
  customLabel ??
  '기본 라벨';

null 또는 undefined일 때만 fallback을 사용합니다.

||는 빈 문자열, 0, false도 fallback 대상으로 취급하므로 의도가 다릅니다.

optional을 남용하면

type TaskItemProps = {
  task?: Task;
  onToggle?:
    (id: string) =>
      void;
  onDelete?:
    (id: string) =>
      void;
};

실제로 필수인 계약까지 optional로 만들면 컴포넌트 내부에 방어 코드가 늘어납니다.

필수 props는 필수로 유지합니다.

exactOptionalPropertyTypes

이 옵션이 켜져 있으면 optional property의 “없음”과 명시적인 undefined 할당을 더 엄격하게 구분합니다.

프로젝트 tsconfig 정책에 따라 동작 차이가 있을 수 있으므로 optional property를 설계할 때 의미를 명확히 합니다.

독립 예제

type UserBadgeProps = {
  name: string;
  description?: string;
  userId:
    string | null;
};


export default function UserBadge({
  name,
  description,
  userId,
}: UserBadgeProps) {
  return (
    <section>
      <strong>
        {name}
      </strong>


      {description ? (
        <p>
          {description}
        </p>
      ) : null}


      <p>
        {userId ??
          '선택 없음'}
      </p>
    </section>
  );
}

학습 완료 체크리스트

기본값, ??, ||가 각각 undefined·null·빈 문자열을 어떻게 처리하는지 설명해 보세요. optional과 명시적 null을 허용하는 union은 같은 계약이 아닙니다.


작은 변형으로 확인하기

label({ label: '' })는 빈 문자열을 반환합니다. 기본값은 undefined일 때만 적용됩니다. null을 허용하도록 타입을 바꾸면 ?? 처리가 필요한 이유를 확인하세요.

선택 실습: 할 일 앱에 적용하기

TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 JavaScript 코드를 TypeScript로 확장합니다.

TaskCard의 선택적 설명과 TaskDetail의 선택 상태를 정리합니다.

선택: 연재 프로젝트와 전체 자료

연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.

TaskFlow 프로젝트 적용

React TypeScript optional props 기본값 처리: undefined·null 차이 · 단계별 TaskFlow

타입을 적용한 TaskFlow에서 작업을 추가·완료 전환·삭제하세요. 타입만 바뀐 단계는 이전과 화면이 같습니다.

예제 크게 보기 (새 탭)

변경 설명과 부분 예제를 읽은 뒤, 시작·완료 파일과 적용 순서에서 필요한 선언·import·화면 연결을 포함한 전체 파일을 확인하세요. 이번 단계의 변경 이유와 적용 위치도 함께 확인합니다.

시작본과 이번 변경

React TypeScript 이벤트 폼 useRef 타입 지정의 완료 체크포인트를 기준으로 이어갑니다. 변경하지 않는 파일은 그 시작 상태를 유지합니다.

src/components/TaskCard.tsx에 선택적 description을 두고 TaskDetail.tsx는 Task를 import해 task: Task | null을 필수 입력으로 받습니다. 누락과 선택 없음의 계약이 다른지 검사합니다.

이전 체크포인트

typescript-step-18-react-events

이번 목표

TaskCard의 선택적 설명과 TaskDetail의 선택 상태를 정리합니다.

TaskCard

type TaskCardProps = {
  title: string;
  description?: string;
};


export default function TaskCard({
  title,
  description,
}: TaskCardProps) {
  return (
    <article>
      <h2>
        {title}
      </h2>


      {description ? (
        <p>
          {description}
        </p>
      ) : null}
    </article>
  );
}

TaskDetail

type TaskDetailProps = {
  task:
    Task | null;
};


export default function TaskDetail({
  task,
}: TaskDetailProps) {
  if (!task) {
    return (
      <p>
        작업을 선택하세요.
      </p>
    );
  }


  return (
    <section>
      <h2>
        {task.title}
      </h2>
    </section>
  );
}

검증

npx tsc --noEmit
npm run build

중간부터 시작하는 전체 파일과 적용 순서

이번 글 시작 ZIP · 이번 글 완료 ZIP

시작 ZIP은 직전 프로젝트 단계 typescript-step-18-react-events의 실제 완료 파일입니다. 프로젝트 순서를 이어가는 기준이며 사이트의 기술 읽기 순서와는 구분합니다.

  1. 시작 ZIP을 새 폴더에 풉니다. 이 폴더가 작업 루트이며 기존 체크포인트 파일을 별도로 찾을 필요가 없습니다.
  2. TaskCard·TaskDetail과 App 사용 위치를 함께 추가합니다. TaskDetail의 Task import를 포함합니다.
  3. 아래 변경 파일을 같은 상대 경로에 추가하거나 전체 교체합니다. 표시하지 않은 파일과 images는 시작본 그대로 유지합니다. package-lock.json은 완료 ZIP에 있는 파일을 함께 적용합니다.
  4. 작업 루트에서 명령을 실행하고 예상 결과를 확인합니다. 완료 ZIP은 직접 적용한 결과를 비교하는 기준입니다.

추가·교체 파일

  • CHANGES.md — 전체 교체
  • README.md — 전체 교체
  • src/App.tsx — 전체 교체
  • src/components/TaskCard.tsx — 추가
  • src/components/TaskDetail.tsx — 추가

실행과 확인

npm ci
npm run dev
npm run typecheck
npm test
npm run build

개발 서버는 실행 상태로 두고 다른 터미널에서 검사 명령을 실행하세요. Node.js 24.18.0에서 검증했습니다. JavaScript는 http://127.0.0.1:4173, TypeScript는 Vite 실행 출력의 주소로 접속합니다.

예상 결과: description 없이 카드 제목을 표시하며 작업을 모두 삭제하면 작업을 선택하세요가 표시됩니다.

기존 ZIP 검증 기록: TypeScript strict 검사·긍정/부정 타입 계약·런타임 검사·Vite 빌드 통과; React 폼·버튼·초점 DOM 검사 통과. 시작 ZIP에 이 글의 변경 파일만 적용한 결과와 완료 ZIP의 모든 파일 내용이 일치했습니다. 브라우저 화면 배치·실제 기기·외부 CDN 가용성은 이 검사에 포함하지 않았습니다.

이 절의 전체 파일은 ZIP과 일치합니다. 위쪽 원문의 짧은 예제나 생략 표시가 있는 부분 코드는 개념·변경 위치 설명용입니다. 서버 저장과 인증은 제공하지 않으며 JSON fixture는 로컬 읽기 실습용입니다.

복사할 변경 파일 전체 코드

src/App.tsx — 전체 파일

import TaskCard from './components/TaskCard.js';
import TaskDetail from './components/TaskDetail.js';
import { useState } from 'react';
import type { Task } from './types/task.js';
import { toggleTask, deleteTask, addTask } from './core/tasks.js';
import TaskForm from './components/TaskForm.js';
import TaskList from './components/TaskList.js';
export default function App() {
 const [tasks,setTasks]=useState<Task[]>([{id:'1',title:'TypeScript 공부',completed:false},{id:'2',title:'React props 확인',completed:true}]);
 const onToggle=(id:string)=>setTasks(current=>toggleTask(current,id));
 const onDelete=(id:string)=>setTasks(current=>deleteTask(current,id));
 return <><TaskCard title="작업 안내"/><TaskDetail task={tasks[0] ?? null}/><TaskForm onAdd={title=>setTasks(current=>addTask(current,title))}/><p>전체 {tasks.length} · 완료 {tasks.filter(task=>task.completed).length}</p><TaskList tasks={tasks} onToggle={onToggle} onDelete={onDelete}/></>;
}

src/components/TaskCard.tsx — 전체 파일

type TaskCardProps = {
  title: string;
  description?: string;
};


export default function TaskCard({
  title,
  description,
}: TaskCardProps) {
  return (
    <article>
      <h2>
        {title}
      </h2>


      {description ? (
        <p>
          {description}
        </p>
      ) : null}
    </article>
  );
}

src/components/TaskDetail.tsx — 전체 파일

import type { Task } from '../types/task.js';
type TaskDetailProps = {
  task:
    Task | null;
};


export default function TaskDetail({
  task,
}: TaskDetailProps) {
  if (!task) {
    return (
      <p>
        작업을 선택하세요.
      </p>
    );
  }


  return (
    <section>
      <h2>
        {task.title}
      </h2>
    </section>
  );
}

완료 체크포인트

typescript-step-19-optional-props

다음 학습

다음 글에서는 children을 ReactNode와 PropsWithChildren으로 표현하는 기준을 정리합니다.

다음 글: TypeScript children 타입: ReactNode와 PropsWithChildren 차이

글 소개·학습 준비·전체 목차
React TypeScript optional props 기본값 처리: undefined·null 차이

TypeScript · 19 / 22 · React 연결 · optional props와 null

prop 생략·undefined·null에는 언제 기본값이 적용될까요? optional prop은 생략할 수 있고 구조 분해 기본값은 undefined일 때만 적용됩니다. 명시적 null은 타입에서 따로 허용하며 ??로 대체할 수 있습니다. 빈 문자열이 기본값과 구분되는 단독 예제로 판단 기준을 확인합니다.

optional prop과 null은 비슷해 보이지만 의미가 다릅니다. undefined는 “값이 전달되지 않음”, null은 “값은 존재하지만 현재 비어 있음”이라는 상태를 표현할 수 있습니다.

이번 글에서 배우는 내용

  • prop?: T
  • T | undefined
  • T | null
  • destructuring 기본값
  • nullish coalescing
  • optional props를 남용하지 않는 기준
  • selected state에 null을 사용하는 이유

단독 학습 선수 지식

TypeScript union·undefined/null와 React props을 알면 됩니다. 이전 TaskFlow 파일 없이 직접 확인 예제로 학습할 수 있습니다.

목차

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 22개 · 필수 21개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

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

RSS 피드 구독하기

댓글 남기기