본문 바로가기

TypeScript

🧠 useState에 타입 주는 가장 깔끔한 패턴 정리 (React + TypeScript)

들어가면서

React에서 useState를 사용할 때, 타입을 어떻게 줘야 가장 안정적이고 깔끔할까?
처음 TS를 쓰기 시작하면 헷갈리는 부분 중 하나인데, 이번 포스팅에서는 다양한 상황에 맞는 useState 타입 선언 패턴을 정리해보겠다.

 

 

✅ 1. 기본 타입 (number, string, boolean)

const [count, setCount] = useState<number>(0);
const [name, setName] = useState<string>('홍길동');
const [isLoading, setIsLoading] = useState<boolean>(false);
  • useState에 초깃값만 넣으면 자동 추론되기도 하지만, 명시하는 게 더 안전할 때도 있음 (초깃값이 null일 때 등)

 

✅ 2. 유니언 타입 (string 또는 null)

const [selected, setSelected] = useState<string | null>(null);
  • 흔히 선택값이 없을 수도 있는 경우에 사용
  • 예: 선택한 드롭다운 항목, 로그인한 사용자 정보 등

 

✅ 3. 객체 타입

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

const [user, setUser] = useState<User>({ id: 1, name: '홍길동' });
  • 객체는 되도록 interface나 type으로 명확히 정의
  • 특히 값이 복잡해질수록 타입 분리해두는 게 유지보수에 유리

 

✅ 4. 배열 타입

const [items, setItems] = useState<string[]>([]);
const [users, setUsers] = useState<User[]>([]);
  • 빈 배열은 TS가 never[]로 추론할 수 있어 타입을 명시적으로 지정해주는 게 좋음

 

✅ 5. 제네릭 객체 예시 (Form 상태 관리)

interface FormState {
  username: string;
  email: string;
  age?: number;
}

const [form, setForm] = useState<FormState>({
  username: '',
  email: '',
});

 

  • 실무에서 흔히 쓰이는 패턴
  • age처럼 optional 필드를 잘 정의하면 편리함

 

 

✅ 6. 리터럴 타입

type Tab = 'home' | 'profile' | 'settings';

const [activeTab, setActiveTab] = useState<Tab>('home');

 

  • setActiveTab('test') 같은 의도하지 않은 값 방지 가능
  • UI 상태 관리에서 매우 유용

 

 

✅ 7. useState 초기값이 null인 경우

const [data, setData] = useState<User | null>(null);

 

 

  • 서버에서 데이터를 비동기로 가져오는 구조일 때 자주 등장
  • 타입 단언 (null as User | null)은 되도록 피하고, 명확히 타입 선언해주기

 

 

 

🛠️ 보너스: 복잡한 상태 관리를 위한 패턴

type Step = 'intro' | 'form' | 'result';
type Status = 'idle' | 'loading' | 'success' | 'error';

interface ProcessState {
  step: Step;
  status: Status;
}

const [process, setProcess] = useState<ProcessState>({
  step: 'intro',
  status: 'idle',
});

 

  • UI 흐름을 enum-like 타입으로 모델링하면 디버깅과 협업이 쉬워짐
  • 실무에선 enum을 직접 쓰는 경우도 많음 (단, 문자열 literal이 더 직관적인 경우도 있음)

 

 

 

📌 정리표

상황  타입 선언 예시
숫자 상태 useState<number>(0)
null 허용 useState<string | null>(null)
객체 상태 useState<User>({...})
배열 상태 useState<string[]>([])
리터럴 제한 useState<'on' | 'off'>('on')
비동기 fetch 대상 useState<DataType | null>(null)

 

 

 

 

💡 마무리하며

  • 초기값이 확정적이면 TS가 자동 추론하므로 타입 생략해도 무방
  • 하지만 null, 빈 배열, 비동기 상태 등은 직접 명시하는 게 안전
  • useState<>()는 상태값보다 상태의 목적에 따라 설계하는 것이 중요