React 컴포넌트에서 Conditional TypeScript Generics

발행: (2026년 1월 9일 오후 11:01 GMT+9)
8 min read
원문: Dev.to

Source: Dev.to

이전 기사 여기에서 저는 TypeScript 제네릭에 대한 논의를 계속하겠다고 약속했습니다 — React 컴포넌트를 다룰 때 특히 중요하다고 생각하는 주제입니다.

그 기사에서는 제네릭 컴포넌트의 기본 정의를 다루고, 제네릭이 언제 그리고 왜 유용한지에 대한 간단한 예시를 살펴보았습니다. 이번 포스트에서는 다음 단계로 조건부 제네릭에 초점을 맞추고, 이것이 실제 React 컴포넌트에 어떻게 적용될 수 있는지 살펴보겠습니다.

여기서는 제가 실제 프로젝트에서 작업하면서 경험한 예시를 살펴보고자 합니다. 이 예시는 제네릭 타입이 얼마나 우아하고 강력한지 보여줍니다.

문제: 제네릭이 없는 선택 컴포넌트

다음은 props에 따라 하나 또는 여러 옵션을 선택할 수 있는 선택 컴포넌트를 고려해 보겠습니다.

// Just an interface of option structure
interface Option {
  id: number;
  name: string;
}

// It's your value type
type Value = Option[] | Option | undefined;

// Component props interface
interface Props {
  value?: Value;
  options?: Option[];
  multiple?: boolean;
  onChange?: (value: Value) => void;
}

이 컴포넌트를 사용할 때 value 타입에 대해 완전히 안전하지 않습니다. TypeScript는 컴포넌트가 다중 선택 모드인지 아닌지 알 수 없기 때문입니다.
value?: ValueonChange?: (value: Value) => voidmultiple?: boolean과 무관하게 두 상태 모두에서 항상 사용할 수 있습니다. 이는 충분히 엄격하지 않으며, 다음과 같은 보장을 원합니다:

  • multiplefalse(또는 생략)인 경우 → value단일 객체(또는 undefined)여야 합니다;
  • multipletrue인 경우 → value배열이어야 합니다.

Props 인터페이스에 제네릭 도입

우리는 동일한 Option 인터페이스를 유지합니다:

interface Option {
  id: number;
  name: string;
}

이제 Props를 두 개의 제네릭 파라미터 OM으로 확장합니다:

interface Props<O extends Option = Option, M extends boolean | undefined = undefined> {
  value?: M extends undefined | false ? O | undefined : O[];
  options?: O[];
  multiple?: M;
  onChange?: (
    values: M extends undefined | false ? O | undefined : O[]
  ) => void;
}

설명

제네릭의미
OOption을 확장; 옵션 타입을 나타냅니다.
M`boolean
valueM extends undefined | false ? O | undefined : O[]multiplefalse이거나 생략된 경우 단일 O(또는 undefined), 그렇지 않으면 배열 O[].
onChange콜백 인수에 대해 동일한 조건부 타입을 사용합니다.

조건부 타입 추출

조건부 표현식을 반복하는 것은 번거롭기 때문에, 이를 자체 제네릭 타입으로 추출합니다:

type Value<O extends Option, M extends boolean | undefined> =
  M extends undefined | false ? O | undefined : O[];

이제 Props가 훨씬 깔끔해집니다:

interface Props<O extends Option = Option, M extends boolean | undefined = undefined> {
  value?: Value<O, M>;
  options?: O[];
  multiple?: M;
  onChange?: (values: Value<O, M>) => void;
}

컴포넌트 자체에 제네릭 추가하기

컴포넌트도 제네릭이어야 하며, 해당 파라미터들을 Props에 전달해야 합니다:

export const SelectionExample = <
  O extends Option = Option,
  M extends boolean | undefined = undefined
>({
  value = undefined,
  multiple = false,
  onChange = () => undefined,
  options = [],
}: Props<O, M>) => {
  const isOptionSelected = (option: O): boolean => {
    // `value`와 `multiple`을 사용한 구현
    return false;
  };

  const handleOptionClick = (option: O) => {
    if (multiple) {
      // `onChange`를 사용한 다중 선택 구현
    } else {
      // `onChange`를 사용한 단일 선택 구현
    }
  };

  return (
    <div>
      {options.map((option) => {
        const selected = isOptionSelected(option);
        return (
          <div key={option.id}>
            {/* 옵션 UI 렌더링 */}
          </div>
        );
      })}
    </div>
  );
};

Note: 제네릭 파라미터 <O, M>은 컴포넌트에 선언된 뒤 Props에 전달됩니다.

제네릭 컴포넌트 사용하기

1️⃣ 다중 선택

export const UsageExample = () => {
  const options: Option[] = [
    { id: 1, name: "First" },
    { id: 2, name: "Second" },
    { id: 3, name: "Third" },
  ];

  return (
    <SelectionExample
      multiple
      options={options}
      onChange={(selected) => {
        // `selected` is inferred as `Option[]`
        console.log("selected:", selected);
      }}
    />
  );
};

2️⃣ 단일 선택

export const SingleUsage = () => {
  const options: Option[] = [
    { id: 1, name: "First" },
    { id: 2, name: "Second" },
    { id: 3, name: "Third" },
  ];

  return (
    <SelectionExample
      options={options}
      onChange={(selected) => {
        // `selected` is inferred as `Option | undefined`
        console.log("selected:", selected);
      }}
    />
  );
};

두 경우 모두 TypeScript가 value의 형태와 onChange의 인자 타입을 올바르게 추론하여, 우리가 원했던 엄격한 보장을 제공합니다.

Takeaways

  • Conditional generics는 props 간의 관계를 표현할 수 있게 해줍니다 (예: multiplevalue 형태).
  • 반복되는 조건부 로직을 재사용 가능한 제네릭 타입(Value)으로 추출하면 코드가 DRY하고 가독성이 유지됩니다.
  • 컴포넌트 자체에 제네릭을 선언하고 이를 props 인터페이스에 전달하면 전체 컴포넌트가 타입‑안전성을 확보합니다.

이 패턴을 사용하면 props 값에 따라 API가 조정되는 고도로 재사용 가능하고 타입‑안전한 React 컴포넌트를 만들 수 있습니다. 즐거운 타입 작성 되세요!

다중 선택 (추가 예시)

export const UsageExample = () => {
  return (
    <>
      {/* ... */}
      {/* `values` is the selected options array */}
      {console.log(values)}
    </>
  );
};

단일 선택 (추가 예시)

export const UsageExample = () => {
  return (
    <>
      {/* ... */}
      {/* `value` is the selected option */}
      {console.log(value)}
    </>
  );
};

잘못된 사용

export const UsageExample = () => {
  return (
    <>
      {/* ... */}
      {/* `values` has type `Option | undefined` – this is a misuse when multiple is expected */}
      {console.log(values)}
    </>
  );
};

이 글이 TypeScript 기능을 더 깊게 파고들도록 만들었을 수도 있습니다. 하지만 직접 비슷한 구현을 마치고 여러분의 컴포넌트가 얼마나 더 강력해졌는지 깨달으면 만족감을 느낄 것이라고 믿습니다.

Back to Blog

관련 글

더 보기 »

React 컴포넌트에서 TypeScript Generics

소개 제네릭은 React 컴포넌트에서 매일 사용하는 것은 아니지만, 특정 경우에는 유연하고 타입‑안전한 컴포넌트를 작성할 수 있게 해줍니다.