useSelect 함수 제작기 (feat: 함수 오버로딩)

date
Dec 8, 2023
thumbnail
slug
useSelect 함수 제작기
author
status
Published
tags
TypeScript
JavaScript
summary
함수 오버로딩을 활용한 useSelect 함수 제작기
type
Post
updatedAt
Dec 12, 2023 11:59 PM

들어가며

스프린트를 진행하다보니 선택 기능에 대한 추상화의 필요성을 느끼게 되었다.
기능과 데이터를 기준으로 컴포넌트를 바라보다보니 단순 select 기능 뿐 아니라 선택 값에 따라 뷰가 변경되는 로직에도 사용할 수 있기에 활용도가 높다고 판단했다.
 
나는 보통 DX를 고려해서 추상화를 진행하는 편이다. 디자인 시스템을 개발하다보니 컴포넌트를 사용하는 유저(개발자)들의 입장에서 어떤 구조로 보여져야 형태나 기능을 잘 이해할 수 있을지 고민하는 편이다.
따라서 아래 기능과 형태를 먼저 정의했다.

개발 목표를 정해보자.

데이터는 오로지 Record<string, unknown>[] 타입만 대응한다.
 
사용하는 입장에서 아래와 같이 간편해야한다.
const { handleToggle, selectedItem } = useSelect();
 
단일 선택, 복수 선택에 대한 기능
const { handleToggle, selectedItem } = useSelect('SINGLE');
const { handleToggle, selectedItem } = useSelect('MULTI');
 
옵션으로 선택 가능한 최대 개수를 정의할 수 있도록 한다
const { handleToggle, selectedItem } = useSelect('MULTI', { max: 5 });
 
(
	mode: Mode,
  options?: { max?: number; },
)
 
return 객체에 대한 정보는 아래와 같이 정의해봤다.
  • 선택된 요소(객체)에 대한 정보: selectedItem
  • 선택 핸들러: handleSelect
  • 해제 핸들러: handleRemove
  • 토글 핸들러: handleToggleSelect
  • 초기화 핸들러: handleReset
selectedItem: T[] | T | null;
handleSelect: (id: string) => void;
handleRemove: (id: string) => void;
handleReset: () => void;
handleToggleSelect: (id: string) => void;
 

객체 중 어떤 키가 id인지 어떻게 알지?

어떤 key가 고유 ID인지 받아도록 변경한다.
selectedItem에서 객체를 제공하고 각 handler들이 id만 받는다면 useSelect 훅도 전체 데이터를 알고 있어야 가능하다
(
	mode: Mode,
	data: T[],
  options?: { key: string; max?: number; },
)
 

MULTI, SINGLE 상태에 따라 타입 분기는 안되나?

문제가 생겼다. SINGLE 타입은 하나의 데이터를 return 하다보니 array가 아닌 객체를 return 하고 있는데 타입스크립트에서는 T[] | T | null 위와 같이 추론된다.
 

함수 오버로딩 활용하기

 
함수 오버로딩이란 같은 이름의 함수에 대해 여러 가지 타입의 매개변수와 반환 타입을 정의할 수 있게 해주는 기능이다.
코드 구현체와 각 조건에 따른 함수 오버로드로 구성되어있다.
아래의 예로 SINGLE, MULTI prop에 따른 타입이 별도로 정의되어있는데 함수를 사용할 때 오버로딩한 조건과 일치한다면 해당 타입으로 변경되는 것이다.
export function useSelect<T>(
  mode: 'SINGLE',
  data: T[],
  options?: { key: string; defaultSelected?: T[] },
): {
  selectedItem: SelectedItem<T, 'SINGLE'>;
  handleToggleSelect: (id: string) => void;
  handleSelect: (id: string) => void;
  handleReset: () => void;
  handleRemove: (id: string) => void;
};
SINGLE 상태 정의
export function useSelect<T>(
  mode: 'MULTI',
  data: T[],
  options?: { key: string; max?: number; defaultSelected?: T[] },
): {
  selectedItem: SelectedItem<T, 'MULTI'>;
  handleToggleSelect: (id: string) => void;
  handleSelect: (id: string) => void;
  handleReset: () => void;
  handleRemove: (id: string) => void;
};
MULTI 상태 정의
type SelectedItem<T, Mode extends 'SINGLE' | 'MULTI'> = Mode extends 'SINGLE' ? T | null : T[];

export function useSelect<T extends { [key: string]: any }, Mode extends 'SINGLE' | 'MULTI'>(
  mode: Mode,
  data: T[],
  options?: { key: string; max?: number; },
): {
... 생략
useSelect 구현체
 
useSelect를 사용하면 아래와 같이 적용된다.
SINGLE일 때는 T | null로 추론, MULTI 일 때는 T[]로 추론된다.
notion image