React + TypeScript — Props, Hooks, Event Handler 타이핑
React + TypeScript에서는 Props, State, Event, Ref 등에 타입을 지정 해서 컴포넌트의 안전성과 자동 완성을 확보합니다.
Props 타이핑
// Props 타입 정의
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
}
// 함수 컴포넌트에 Props 적용
function Button({ label, onClick, variant = 'primary', disabled = false }: ButtonProps) {
return (
<button
className={`btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
}
children Props
import { ReactNode } from 'react';
interface CardProps {
title: string;
children: ReactNode; // JSX, 문자열, 숫자, 배열, null 등
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}
제네릭 컴포넌트
interface ListProps<T> {
items: T[];
renderItem: (item: T) => ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// 사용 — T가 자동 추론됨
<List
items={[{ id: 1, name: '홍길동' }]}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => String(user.id)}
/>
Hooks 타이핑
useState
// 초기값에서 타입 추론
const [count, setCount] = useState(0); // number
// 타입 명시가 필요한 경우
const [user, setUser] = useState<User | null>(null);
// 초기값이 null이므로 타입을 명시해야 함
// 유니온 타입
const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle');
useRef
// DOM 요소 참조
const inputRef = useRef<HTMLInputElement>(null);
function focusInput() {
inputRef.current?.focus(); // HTMLInputElement | null
}
// 값 저장용 (mutable ref)
const intervalRef = useRef<number | null>(null);
function startTimer() {
intervalRef.current = window.setInterval(() => {}, 1000);
}
useReducer
type State = { count: number; error: string | null };
type Action =
| { type: 'INCREMENT'; payload: number }
| { type: 'DECREMENT'; payload: number }
| { type: 'ERROR'; message: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + action.payload };
case 'DECREMENT':
return { ...state, count: state.count - action.payload };
case 'ERROR':
return { ...state, error: action.message };
}
}
const [state, dispatch] = useReducer(reducer, { count: 0, error: null });
dispatch({ type: 'INCREMENT', payload: 1 }); // 타입 안전
이벤트 핸들러 타이핑
import { ChangeEvent, FormEvent, MouseEvent } from 'react';
function Form() {
// input change 이벤트
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
// form submit 이벤트
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
// button click 이벤트
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
console.log(e.clientX, e.clientY);
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<button onClick={handleClick}>제출</button>
</form>
);
}
인라인 이벤트 핸들러
// 인라인에서는 타입이 자동 추론됨
<input onChange={(e) => console.log(e.target.value)} />
// e: ChangeEvent<HTMLInputElement> — 자동 추론
// 별도 함수로 빼면 타입 명시 필요
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
HTML 속성 확장
import { ComponentPropsWithoutRef, ComponentPropsWithRef } from 'react';
// HTML button의 모든 속성을 상속
interface CustomButtonProps extends ComponentPropsWithoutRef<'button'> {
variant?: 'primary' | 'secondary';
}
function CustomButton({ variant = 'primary', children, ...rest }: CustomButtonProps) {
return (
<button className={`btn-${variant}`} {...rest}>
{children}
</button>
);
}
// 모든 button 속성 사용 가능
<CustomButton onClick={() => {}} disabled type="submit" variant="primary">
클릭
</CustomButton>
Context 타이핑
import { createContext, useContext } from 'react';
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
// 안전한 커스텀 훅
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth는 AuthProvider 안에서 사용해야 합니다');
}
return context;
}
정리
- Props는
interface나type으로 정의하고,children은ReactNode를 사용한다 useState는 초기값에서 추론하되, null 가능성이 있으면 타입을 명시한다- 이벤트 핸들러는
ChangeEvent,FormEvent,MouseEvent등으로 타이핑한다 ComponentPropsWithoutRef로 HTML 요소의 속성을 상속받을 수 있다- Context는 null 체크를 포함한 커스텀 훅으로 감싸는 것이 안전하다
댓글 로딩 중...