React 입력 이벤트와 폼, input ref에는 어떤 TypeScript 타입을 지정하나요?
React 입력 이벤트의 타입은 이벤트가 연결된 DOM 요소에 맞춥니다. 입력창에는 ChangeEvent<HTMLInputElement>, 입력창 ref에는 HTMLInputElement와 null 가능성을 표현합니다.
적용 조건·범위: ref.current가 null일 수 있다는 타입을 없애려고 단언하는 대신 DOM이 연결된 뒤 발생하는 버튼 이벤트에서 접근합니다. optional chaining은 아직 요소가 없는 경우 호출을 건너뛰게 하며, 렌더 중에 focus를 실행하는 문제까지 해결하는 방법은 아닙니다.
직접 확인: 오류에서 수정까지
React TypeScript 이벤트 폼 useRef 타입 지정 · 독립 실습
얻을 결과: ChangeEvent<HTMLInputElement>와 useRef<HTMLInputElement>(null)로 입력과 초점 대상을 구분합니다.
예제 크게 보기 (새 탭)단독 타입 검사에도 React 타입 선언과 JSX 설정이 필요합니다. React Vite 시작 글의 React+TypeScript 환경을 준비한 뒤 아래 수정 예제를 src/App.tsx에 저장합니다. 기존 파일의 내용과 섞지 않습니다. 아래 mount 연결을 함께 추가하면 화면에서도 확인할 수 있습니다.
독자 질문: 입력창의 이벤트와 ref를 어떤 DOM 요소 타입으로 지정할까요?
얻을 결과: ChangeEvent<HTMLInputElement>와 useRef<HTMLInputElement>(null)로 입력과 초점 대상을 구분합니다.
React와 타입 선언이 설치된 프로젝트의 .tsx 파일에서 실행합니다.
import { useRef, type ChangeEvent } from 'react';
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
const onChange = (event: ChangeEvent<HTMLInputElement>) => console.log(event.currentTarget.value);
inputRef.current.focus();
return <input ref={inputRef} onChange={onChange} />;
}
예상 오류 (TS18047): inputRef.current는 null일 수 있습니다. 이벤트 이후 optional chaining으로 접근합니다.
오류 예제를 다음 코드로 교체합니다.
import { useRef, type ChangeEvent } from 'react';
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
const onChange = (event: ChangeEvent<HTMLInputElement>) => console.log(event.currentTarget.value);
const focus = () => inputRef.current?.focus();
return <><input ref={inputRef} onChange={onChange} /><button type="button" onClick={focus}>초점</button></>;
}
npx tsc -p . --noEmit
기존 최소 예제 확인 기록: TypeScript 7.0.2와 설치된 React 타입 선언의 별도 최소 예제에서 오류 TS18047, 수정 후 exit 0 확인. TaskFlow 전체 체크포인트·React 앱 빌드는 미확인.
다음 학습: 19회차에서 생략 가능한 props와 명시적 null을 구분합니다.
inline handler는 잘 추론됩니다
<input
onChange={(event) => {
console.log(
event.target.value,
);
}}
/>
JSX에서 직접 작성한 handler는 input 요소를 기준으로 event 타입을 추론할 수 있습니다.
별도 함수로 분리하면 타입을 명시할 수 있습니다.
ChangeEvent
import type {
ChangeEvent,
} from 'react';
function handleChange(
event:
ChangeEvent<HTMLInputElement>,
) {
console.log(
event.target.value,
);
}
textarea:
ChangeEvent<HTMLTextAreaElement>
select:
ChangeEvent<HTMLSelectElement>
실제 element 타입을 사용합니다.
FormEvent
import type {
FormEvent,
} from 'react';
function handleSubmit(
event:
FormEvent<HTMLFormElement>,
) {
event.preventDefault();
}
form submit 이벤트임을 타입에 표현합니다.
currentTarget과 target
currentTarget은 handler가 연결된 요소를 의미하므로 타입이 더 안정적인 경우가 많습니다.
function handleSubmit(
event:
FormEvent<HTMLFormElement>,
) {
const form =
event.currentTarget;
}
target은 실제 이벤트가 시작된 자식 요소일 수 있습니다.
이 차이를 알고 사용합니다.
useRef
input DOM을 참조합니다.
import {
useRef,
} from 'react';
const inputRef =
useRef<HTMLInputElement>(
null,
);
초기 렌더 시 DOM이 아직 연결되지 않았으므로 null이 포함됩니다.
inputRef.current?.focus();
optional chaining으로 안전하게 호출할 수 있습니다.
ref 타입을 HTMLElement로 넓게 쓰지 않기
실제 input API를 사용할 계획이라면 HTMLInputElement가 더 정확합니다.
useRef<HTMLInputElement>(
null,
)
value, select 등의 input 전용 property를 타입 안전하게 사용할 수 있습니다.
event any 문제
function handleChange(
event: any,
) {
...
}
작성하기는 편하지만 event API 오타를 잡을 수 없습니다.
실제 DOM element를 기준으로 타입을 지정합니다.
독립 예제
import {
useRef,
useState,
} from 'react';
import type {
ChangeEvent,
FormEvent,
} from 'react';
export default function SearchForm() {
const [keyword, setKeyword] =
useState('');
const inputRef =
useRef<HTMLInputElement>(
null,
);
const handleChange = (
event:
ChangeEvent<HTMLInputElement>,
) => {
setKeyword(
event.target.value,
);
};
const handleSubmit = (
event:
FormEvent<HTMLFormElement>,
) => {
event.preventDefault();
inputRef.current?.focus();
};
return (
<form
onSubmit={
handleSubmit
}
>
<input
ref={inputRef}
value={keyword}
onChange={
handleChange
}
/>
</form>
);
}
ref.current가 null일 수 있다는 타입을 없애려고 단언하는 대신 DOM이 연결된 뒤 발생하는 버튼 이벤트에서 접근합니다. optional chaining은 아직 요소가 없는 경우 호출을 건너뛰게 하며, 렌더 중에 focus를 실행하는 문제까지 해결하는 방법은 아닙니다.
원리 확인: React TypeScript 이벤트·DOM ref 안내 (2026-10-12). 아래 기대 결과와 기존 실행 기록은 구분해서 읽습니다.
학습 완료 체크리스트
input의 change, form의 submit, input DOM ref를 각각 구분해 설명해 보세요. ref가 아직 연결되지 않은 상태와 handler의 currentTarget을 같은 값으로 취급하지 않는 것이 완료 기준입니다.
수정 예제의 화면 연결
export default Form;
개발 서버에서 문자를 입력하면 Console에 입력값이 출력되고 초점 버튼을 누르면 입력창에 포커스가 돌아옵니다.
작은 변형으로 확인하기
수정 Form의 ref를 HTMLDivElement로 바꾸면 input ref 연결이 맞지 않습니다. HTMLInputElement로 복원하고 초점 버튼 이벤트에서만 focus가 호출되는지 확인하세요.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 JavaScript 코드를 TypeScript로 확장합니다.
TaskForm의 이벤트와 input ref를 명시적으로 타입화합니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow 프로젝트 적용
React TypeScript 이벤트 폼 useRef 타입 지정 · 단계별 TaskFlow
타입을 적용한 TaskFlow에서 작업을 추가·완료 전환·삭제하세요. 타입만 바뀐 단계는 이전과 화면이 같습니다.
예제 크게 보기 (새 탭)시작·완료 파일과 적용 순서에서 이번 변경 파일의 전체 내용과 적용 순서를 확인합니다.
시작본과 이번 변경
React TypeScript 기본 구조: tsx 파일과 props 타입 기준의 완료 체크포인트를 기준으로 이어갑니다. 변경하지 않는 파일은 그 시작 상태를 유지합니다.
src/components/TaskForm.tsx에서 change·submit·ref 타입을 각각 입력창과 form에 맞춥니다. 빈 값 제출은 onAdd를 부르지 않고 입력창으로 포커스가 돌아와야 합니다.
이전 체크포인트
typescript-step-17-react-props
이번 목표
TaskForm의 이벤트와 input ref를 명시적으로 타입화합니다.
전체 파일·코드 보기
src/components/TaskForm.tsx
import {
useRef,
useState,
} from 'react';
import type {
ChangeEvent,
FormEvent,
} from 'react';
type TaskFormProps = {
onAdd:
(title: string) =>
void;
};
export default function TaskForm({
onAdd,
}: TaskFormProps) {
const [title, setTitle] =
useState('');
const inputRef =
useRef<HTMLInputElement>(
null,
);
const handleChange = (
event:
ChangeEvent<HTMLInputElement>,
) => {
setTitle(
event.target.value,
);
};
const handleSubmit = (
event:
FormEvent<HTMLFormElement>,
) => {
event.preventDefault();
const value =
title.trim();
if (!value) {
inputRef.current
?.focus();
return;
}
onAdd(value);
setTitle('');
inputRef.current
?.focus();
};
return (
<form
onSubmit={
handleSubmit
}
>
<input
ref={inputRef}
value={title}
onChange={
handleChange
}
/>
<button
type="submit"
>
추가
</button>
</form>
);
}
검증
npx tsc --noEmit
npm run build
중간부터 시작하는 전체 파일과 적용 순서
시작 ZIP은 직전 프로젝트 단계 typescript-step-17-react-props의 실제 완료 파일입니다. 프로젝트 순서를 이어가는 기준이며 사이트의 기술 읽기 순서와는 구분합니다.
- 시작 ZIP을 새 폴더에 풉니다. 이 폴더가 작업 루트이며 기존 체크포인트 파일을 별도로 찾을 필요가 없습니다.
- TaskForm을 추가하고 App의 onAdd를 기존 typed addTask에 연결합니다. 보강 전체본은 input의 접근 가능한 이름도 제공합니다.
- 아래 변경 파일을 같은 상대 경로에 추가하거나 전체 교체합니다. 표시하지 않은 파일과 images는 시작본 그대로 유지합니다. package-lock.json은 완료 ZIP에 있는 파일을 함께 적용합니다.
- 작업 루트에서 명령을 실행하고 예상 결과를 확인합니다. 완료 ZIP은 직접 적용한 결과를 비교하는 기준입니다.
추가·교체 파일
CHANGES.md— 전체 교체README.md— 전체 교체src/App.tsx— 전체 교체src/components/TaskForm.tsx— 추가
실행과 확인
npm ci
npm run dev
npm run typecheck
npm test
npm run build
개발 서버는 실행 상태로 두고 다른 터미널에서 검사 명령을 실행하세요. Node.js 24.18.0에서 검증했습니다. JavaScript는 http://127.0.0.1:4173, TypeScript는 Vite 실행 출력의 주소로 접속합니다.
예상 결과: 공백은 추가하지 않고 초점을 유지합니다. 유효 입력은 추가 후 비우고 입력으로 초점을 돌립니다.
기존 ZIP 검증 기록: TypeScript strict 검사·긍정/부정 타입 계약·런타임 검사·Vite 빌드 통과; React 폼·버튼·초점 DOM 검사 통과. 시작 ZIP에 이 글의 변경 파일만 적용한 결과와 완료 ZIP의 모든 파일 내용이 일치했습니다. 브라우저 화면 배치·실제 기기·외부 CDN 가용성은 이 검사에 포함하지 않았습니다.
이 절의 전체 파일은 ZIP과 일치합니다. 위쪽 원문의 짧은 예제나 생략 표시가 있는 부분 코드는 개념·변경 위치 설명용입니다. 서버 저장과 인증은 제공하지 않으며 JSON fixture는 로컬 읽기 실습용입니다.
복사할 변경 파일 전체 코드
src/App.tsx — 전체 파일
import { useState } from 'react';
import type { Task } from './types/task.js';
import { toggleTask, deleteTask, addTask } from './core/tasks.js';
import TaskForm from './components/TaskForm.js';
import TaskList from './components/TaskList.js';
export default function App() {
const [tasks,setTasks]=useState<Task[]>([{id:'1',title:'TypeScript 공부',completed:false},{id:'2',title:'React props 확인',completed:true}]);
const onToggle=(id:string)=>setTasks(current=>toggleTask(current,id));
const onDelete=(id:string)=>setTasks(current=>deleteTask(current,id));
return <><TaskForm onAdd={title=>setTasks(current=>addTask(current,title))}/><p>전체 {tasks.length} · 완료 {tasks.filter(task=>task.completed).length}</p><TaskList tasks={tasks} onToggle={onToggle} onDelete={onDelete}/></>;
}
src/components/TaskForm.tsx — 전체 파일
import {
useRef,
useState,
} from 'react';
import type {
ChangeEvent,
FormEvent,
} from 'react';
type TaskFormProps = {
onAdd:
(title: string) =>
void;
};
export default function TaskForm({
onAdd,
}: TaskFormProps) {
const [title, setTitle] =
useState('');
const inputRef =
useRef<HTMLInputElement>(
null,
);
const handleChange = (
event:
ChangeEvent<HTMLInputElement>,
) => {
setTitle(
event.target.value,
);
};
const handleSubmit = (
event:
FormEvent<HTMLFormElement>,
) => {
event.preventDefault();
const value =
title.trim();
if (!value) {
inputRef.current
?.focus();
return;
}
onAdd(value);
setTitle('');
inputRef.current
?.focus();
};
return (
<form
onSubmit={
handleSubmit
}
>
<input
aria-label="작업 이름"
ref={inputRef}
value={title}
onChange={
handleChange
}
/>
<button
type="submit"
>
추가
</button>
</form>
);
}
완료 체크포인트
typescript-step-18-react-events
다음 학습
다음 글에서는 optional props의 undefined 가능성과 명시적 null을 구분하고 기본값 처리 기준을 정리합니다.
다음 글: React TypeScript optional props 기본값 처리: undefined·null 차이
글 소개·학습 준비·전체 목차

TypeScript · 18 / 22 · React 연결 · 이벤트·폼·useRef
입력 이벤트와 ref에는 어떤 타입을 지정할까요? handler가 연결된 DOM 요소를 기준으로 ChangeEvent<HTMLInputElement>와 FormEvent<HTMLFormElement>를 고르고, input ref는 HTMLInputElement와 null 가능성을 함께 표현합니다. 아래 Form 하나로 입력값 읽기와 버튼 초점 이동을 확인합니다.
이번 글에서 배우는 내용
- ChangeEvent
- FormEvent
- currentTarget과 target
- useRef<HTMLInputElement>
- null 처리
- inline handler의 타입 추론
- 이벤트를 any로 두지 않는 기준
단독 학습 선수 지식
React state·이벤트·ref, .tsx와 DOM 요소 타입을 알면 됩니다. 이전 TaskFlow 파일 없이 직접 확인 예제로 학습할 수 있습니다.
목차
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 22개 · 필수 21개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.