10.3 이벤트 핸들러 타입 — SyntheticEvent와 React 이벤트
React의 합성 이벤트 (SyntheticEvent)
React는 브라우저 네이티브 이벤트를 감싸는 **합성 이벤트(SyntheticEvent)**를 사용합니다. TypeScript에서 이벤트 타입을 정확히 지정해야 올바른 자동완성을 받을 수 있습니다.
import { SyntheticEvent, ChangeEvent, MouseEvent, FormEvent } from 'react';
주요 이벤트 타입
ChangeEvent — 입력 값 변경
// ChangeEvent<HTMLElement>: input, select, textarea에 사용
function InputExample() {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value); // string
console.log(e.target.checked); // boolean (checkbox용)
console.log(e.target.files); // FileList | null (file input용)
};
const handleSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
console.log(e.target.value); // 선택된 option의 value
console.log(e.target.selectedOptions); // HTMLCollectionOf<HTMLOptionElement>
};
const handleTextareaChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
console.log(e.target.value);
};
return (
<>
<input onChange={handleChange} />
<select onChange={handleSelectChange}>
<option value="a">A</option>
<option value="b">B</option>
</select>
<textarea onChange={handleTextareaChange} />
</>
);
}
MouseEvent — 마우스 이벤트
function MouseExample() {
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
console.log(e.clientX, e.clientY); // 마우스 위치
console.log(e.button); // 0: 왼쪽, 1: 가운데, 2: 오른쪽
console.log(e.shiftKey); // Shift 키 눌림 여부
console.log(e.currentTarget); // HTMLButtonElement
};
const handleDivClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation(); // 이벤트 버블링 중지
e.preventDefault(); // 기본 동작 방지
};
return (
<div onClick={handleDivClick}>
<button onClick={handleClick}>클릭</button>
</div>
);
}
FormEvent — 폼 제출
interface LoginForm {
email: string;
password: string;
}
function LoginForm() {
const [form, setForm] = useState<LoginForm>({ email: '', password: '' });
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); // 기본 폼 제출 방지
const formData = new FormData(e.currentTarget);
const email = formData.get('email') as string;
const password = formData.get('password') as string;
await login({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
type="email"
value={form.email}
onChange={e => setForm(prev => ({ ...prev, email: e.target.value }))}
/>
<input
name="password"
type="password"
value={form.password}
onChange={e => setForm(prev => ({ ...prev, password: e.target.value }))}
/>
<button type="submit">로그인</button>
</form>
);
}
자주 쓰는 이벤트 타입 목록
import {
// 마우스
MouseEvent,
MouseEventHandler, // (e: MouseEvent) => void 의 별칭
// 키보드
KeyboardEvent,
KeyboardEventHandler,
// 입력
ChangeEvent,
ChangeEventHandler,
// 폼
FormEvent,
FormEventHandler,
// 포커스
FocusEvent,
FocusEventHandler,
// 드래그
DragEvent,
DragEventHandler,
// 터치
TouchEvent,
TouchEventHandler,
// 휠
WheelEvent,
WheelEventHandler,
// 클립보드
ClipboardEvent,
ClipboardEventHandler,
// 스크롤
UIEvent,
} from 'react';
이벤트 핸들러를 Props로 전달
// 이벤트 핸들러 타입을 Props에 포함
interface ButtonProps {
label: string;
onClick?: MouseEventHandler<HTMLButtonElement>;
onMouseEnter?: MouseEventHandler<HTMLButtonElement>;
}
// 또는 더 구체적으로
interface InputProps {
value: string;
onChange: ChangeEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>;
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
}
function Input({ value, onChange, onBlur, onKeyDown }: InputProps) {
return (
<input
value={value}
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
/>
);
}
실전 예제: 완전한 폼 컴포넌트
import { useState, ChangeEvent, FormEvent, FocusEvent } from 'react';
interface Field {
value: string;
error: string;
touched: boolean;
}
interface FormState {
name: Field;
email: Field;
message: Field;
}
type FieldName = keyof FormState;
function ContactForm() {
const [form, setForm] = useState<FormState>({
name: { value: '', error: '', touched: false },
email: { value: '', error: '', touched: false },
message: { value: '', error: '', touched: false },
});
const validate = (name: FieldName, value: string): string => {
switch (name) {
case 'name':
return value.length < 2 ? '이름은 2자 이상이어야 합니다' : '';
case 'email':
return !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
? '올바른 이메일 주소를 입력하세요'
: '';
case 'message':
return value.length < 10 ? '메시지는 10자 이상이어야 합니다' : '';
default:
return '';
}
};
const handleChange = (name: FieldName) =>
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { value } = e.target;
setForm(prev => ({
...prev,
[name]: {
...prev[name],
value,
error: prev[name].touched ? validate(name, value) : '',
},
}));
};
const handleBlur = (name: FieldName) =>
(_: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setForm(prev => ({
...prev,
[name]: {
...prev[name],
touched: true,
error: validate(name, prev[name].value),
},
}));
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 전체 유효성 검사
const hasErrors = Object.entries(form).some(
([name, field]) => validate(name as FieldName, field.value) !== ''
);
if (hasErrors) return;
console.log('제출:', {
name: form.name.value,
email: form.email.value,
message: form.message.value,
});
};
return (
<form onSubmit={handleSubmit}>
{(['name', 'email'] as const).map(field => (
<div key={field}>
<input
value={form[field].value}
onChange={handleChange(field)}
onBlur={handleBlur(field)}
placeholder={field === 'name' ? '이름' : '이메일'}
/>
{form[field].touched && form[field].error && (
<span className="error">{form[field].error}</span>
)}
</div>
))}
<textarea
value={form.message.value}
onChange={handleChange('message')}
onBlur={handleBlur('message')}
placeholder="메시지"
/>
{form.message.touched && form.message.error && (
<span className="error">{form.message.error}</span>
)}
<button type="submit">보내기</button>
</form>
);
}
파일 업로드 이벤트
function FileUpload() {
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
const file = files[0];
console.log(file.name); // string
console.log(file.size); // number (bytes)
console.log(file.type); // string (MIME type)
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target?.result; // string | ArrayBuffer | null
console.log(result);
};
reader.readAsDataURL(file);
};
return (
<input
type="file"
accept="image/*"
onChange={handleFileChange}
/>
);
}
고수 팁
1. 이벤트 핸들러 팩토리 패턴
// 여러 필드를 처리하는 범용 핸들러
function useFormHandlers<T extends Record<string, string>>(
setForm: React.Dispatch<React.SetStateAction<T>>
) {
const handleChange = (field: keyof T) =>
(e: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
setForm(prev => ({ ...prev, [field]: e.target.value }));
};
return { handleChange };
}
2. 이벤트 타겟 타입 캐스팅
// 이벤트 타겟이 특정 요소임을 확신할 때
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLButtonElement;
console.log(target.dataset.id);
};