React — 가장 많이 사용하는 타입들 알아보기
공식문서 기반의 타입스크립트 입문기
들어가며 (React에서 제공하는 주요 타입들)
React로 컴포넌트를 개발하다 보면 자연스럽게 마주치는 타입들이 있습니다. ReactNode, PropsWithChildren, ChangeEvent 같은 것들이죠. 이 타입들은 React가 제공하는 인터페이스의 일부로, 컴포넌트 개발을 더 안전하고 편리하게 만들어줍니다.
이번 편에서는 React 개발에서 가장 많이 사용하는 타입들을 소개하고, 각 타입이 어떤 역할을 하는지 가볍게 살펴보겠습니다. 특히 개발자가 명시적으로 타입을 지정해서 사용해야 하는 ReactNode, PropsWithChildren, ComponentProps, ChangeEvent<HTMLInputElement> 등의 제네릭 타입을 중심으로 설명하겠습니다. 타입스크립트를 처음 접하는 분들도 쉽게 따라올 수 있도록, 실제 사용 예시와 함께 설명하겠습니다.
주요 타입 소개
React의 타입들은 크게 두 가지로 나눌 수 있습니다. 먼저 TypeScript가 상황에 따라 자동으로 추론하거나 적용하는 타입들을 살펴보고, 그 다음에 개발자가 명시적으로 지정해야 하는 타입들을 자세히 알아보겠습니다.
명시적으로 지정하지 않아도 되는 타입들
이 타입들은 JSX에서 자동으로 추론되거나, React.createElement를 사용할 때 자동으로 적용됩니다. 컴포넌트를 사용하는 입장에서는 크게 신경 쓰지 않아도 되는 타입들입니다.
렌더링 타입: ReactElement, JSX.Element
JSX를 사용할 때 자동으로 적용되는 타입들입니다.
-
ReactElement: JSX로 생성된 React 엘리먼트를 표현하는 타입입니다. JSX를 작성하면 TypeScript가 자동으로
React.createElement()의 반환값 타입으로 추론합니다. -
JSX.Element: JSX 문법으로 작성된 엘리먼트의 타입입니다. JSX.Element는 ReactElement의 하위 타입으로, JSX에서 생성되는 엘리먼트들을 자동으로 타입화합니다.
기본 이벤트 타입들
단순한 이벤트 핸들러를 사용할 때는 제네릭 파라미터 없이도 기본적인 타입이 적용됩니다.
- 기본 이벤트 타입들:
onClick,onChange등의 이벤트 핸들러에서 제네릭 파라미터를 생략하면 HTMLElement 기반의 기본 이벤트 타입이 자동으로 적용됩니다.
명시적으로 지정해야 하는 타입들
이 타입들은 컴포넌트 인터페이스를 정의하거나, 제네릭 타입 파라미터를 사용할 때 직접 지정해야 합니다. React 컴포넌트 개발에서 가장 많이 작성하게 되는 타입들입니다.
렌더링 타입: ReactNode
컴포넌트의 children이나 return 타입을 정의할 때 사용하는 가장 넓은 렌더링 타입입니다.
- ReactNode: React가 렌더링할 수 있는 모든 것을 포함하는 가장 넓은 타입입니다. children prop이나 함수의 반환 타입으로 사용할 때
ReactNode를 직접 지정해야 합니다.
Props 타입: PropsWithChildren, ComponentProps
React에서는 다양한 props 관련 유틸리티 타입을 제공하지만, 여기서는 가장 실용적인 몇 가지 타입만 소개하겠습니다. 컴포넌트의 props 인터페이스를 정의할 때 사용하는 유틸리티 타입들입니다.
-
PropsWithChildren: children prop을 포함하는 컴포넌트의 props 타입을 정의합니다.
interface Props extends PropsWithChildren처럼 인터페이스 선언 시 직접 지정해야 합니다. -
ComponentProps: 특정 컴포넌트나 HTML 요소의 props 타입을 추출하는 유틸리티 타입입니다.
ComponentProps에 제네릭 파라미터로 요소 이름을 전달하여 직접 지정해야 합니다 (예:ComponentProps와 input 요소).
이벤트 타입: ChangeEvent, MouseEvent, KeyboardEvent 등
React에서는 다양한 이벤트 타입을 제공하지만, 여기서는 가장 많이 사용하는 몇 가지 이벤트 타입만 소개하겠습니다. 정확한 타입 안전성을 위해 요소 타입을 지정해야 하는 이벤트 타입들입니다.
-
ChangeEvent: 폼 요소의 변경 이벤트를 표현하는 제네릭 타입입니다.
ChangeEvent<HTMLInputElement>처럼 요소 타입을 직접 지정해야 합니다. -
MouseEvent: 마우스 관련 이벤트를 표현하는 제네릭 타입입니다.
MouseEvent<HTMLButtonElement>처럼 요소 타입을 직접 지정해야 합니다. -
KeyboardEvent: 키보드 이벤트를 표현하는 제네릭 타입입니다.
KeyboardEvent<HTMLInputElement>처럼 요소 타입을 직접 지정해야 합니다.
실전 패턴 (폼 컴포넌트 계층 구축하기)
이제 하나의 시나리오를 따라 각 타입들을 어떻게 적용하는지 살펴보겠습니다. 폼 컴포넌트 계층을 구축하면서, 각 타입의 역할을 확인해 보겠습니다.
1. 기본 컴포넌트 구조와 PropsWithChildren 설정
PropsWithChildren은 children prop을 포함하는 컴포넌트의 props 타입을 정의하는 유틸리티 타입입니다. 복합 컴포넌트를 만들 때 children의 타입을 명시적으로 지정해서, 컴포넌트 사용자가 어떤 콘텐츠를 넣을 수 있는지 명확히 할 수 있습니다. 이를 통해 컴포넌트의 인터페이스를 더 안전하게 정의할 수 있습니다.
import React, { PropsWithChildren } from 'react';
interface FormProps extends PropsWithChildren {
onSubmit: () => void;
}
function Form({ children, onSubmit }: FormProps) {
return (
<form onSubmit={onSubmit}>
{children}
</form>
);
}2. ComponentProps로 HTML 요소 속성 확장
ComponentProps는 특정 컴포넌트나 HTML 요소의 props 타입을 추출하는 유틸리티 타입입니다. ComponentProps에 input 요소를 제네릭 파라미터로 전달하면 HTMLInputElement의 모든 속성을 포함하는 타입이 됩니다. 이를 활용하면 HTML 요소의 속성을 완전히 상속하면서 추가적인 props를 정의할 수 있어, 기존 HTML 요소를 확장한 컴포넌트를 쉽게 만들 수 있습니다.
import React, { ComponentProps } from 'react';
type InputProps = ComponentProps<'input'> & {
label: string;
};
function Input({ label, ...inputProps }: InputProps) {
return (
<div>
<label>{label}</label>
<input {...inputProps} />
</div>
);
}3. ChangeEvent로 이벤트 핸들러 타입화
ChangeEvent는 폼 요소의 변경 이벤트를 표현하는 타입입니다. 제네릭 타입 파라미터로 요소 타입을 지정할 수 있어(ChangeEvent<HTMLInputElement>), 이벤트 객체의 target 속성이 특정 HTML 요소 타입임을 보장합니다. 이를 통해 이벤트 핸들러에서 target.value 같은 속성에 타입 안전하게 접근할 수 있습니다.
import React, { useState } from 'react';
function UserForm() {
const [name, setName] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value); // 타입 안전하게 value 접근
};
return (
<input
value={name}
onChange={handleChange}
/>
);
}4. MouseEvent로 마우스 이벤트 처리
MouseEvent는 마우스 관련 이벤트를 표현하는 타입입니다. 제네릭 타입 파라미터로 요소 타입을 지정할 수 있어(MouseEvent<HTMLButtonElement>), 이벤트가 발생한 요소의 타입을 보장합니다. 클릭, 더블클릭, 마우스 이동 등의 이벤트에 유용합니다.
import React from 'react';
function InteractiveButton() {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log('버튼 클릭됨:', e.currentTarget); // HTMLButtonElement 타입 보장
};
const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
console.log('마우스 진입:', e.clientX, e.clientY); // 마우스 좌표 접근
};
return (
<div onMouseEnter={handleMouseEnter}>
<button onClick={handleClick}>
클릭해보세요
</button>
</div>
);
}5. ReactNode로 유연한 콘텐츠 렌더링
ReactNode는 React가 렌더링할 수 있는 모든 것을 포함하는 가장 넓은 타입입니다. 문자열, 숫자, JSX 엘리먼트, 배열, null, undefined까지 모두 포함합니다. children prop으로 ReactNode를 사용하면 컴포넌트가 매우 유연해져, 어떤 종류의 콘텐츠든 받아들일 수 있습니다. 이는 라이브러리 컴포넌트를 만들 때 특히 유용합니다.
import React, { ReactNode } from 'react';
interface ModalProps {
children: ReactNode; // 문자열, JSX, null 등 모두 허용
}
function Modal({ children }: ModalProps) {
return (
<div>
<h2>Modal</h2>
{children}
</div>
);
}
// 사용
<Modal>
{"문자열"}
<span>JSX 엘리먼트</span>
{null}
</Modal>함정과 주의사항
ReactNode vs ReactElement의 함정
ReactNode와 ReactElement는 둘 다 React 컴포넌트의 children을 타입화할 때 자주 사용하는데, 이 둘의 차이를 잘못 이해하면 컴포넌트 사용법이 제한적으로 변합니다.
ReactElement는 JSX로 생성된 엘리먼트만 허용합니다. 예를 들어 <div>내용</div> 같은 JSX만 받을 수 있고, 단순한 문자열이나 숫자는 받을 수 없습니다. 엄격한 타입 검사를 원할 때 유용하지만, 컴포넌트의 유연성이 떨어집니다.
반면 ReactNode는 훨씬 더 넓은 범위를 허용합니다. 문자열("안녕하세요"), 숫자(42), JSX 엘리먼트, null, undefined, 심지어 배열까지 모두 받아들입니다. 라이브러리 컴포넌트를 만들 때는 ReactNode를 사용하는 것이 일반적입니다.
// ❌ ReactElement는 JSX만 허용
interface StrictProps {
children: React.ReactElement;
}
<StrictProps>{"문자열"} </StrictProps> // 타입 에러
// ✅ ReactNode는 다양한 타입 허용
interface FlexibleProps {
children: React.ReactNode;
}
<FlexibleProps>{"문자열"} <span>JSX</span> {null}</FlexibleProps>ComponentProps의 함정
ComponentProps는 HTML 요소의 모든 속성을 편리하게 가져올 수 있게 해주지만, React에서만 사용하는 특별한 props들은 자동으로 포함되지 않는다는 점을 주의해야 합니다.
예를 들어 ComponentProps에 input 요소를 전달하면 type, value, onChange, placeholder 같은 HTML input의 모든 표준 속성을 얻을 수 있습니다. 하지만 React의 ref나 key 같은 속성들은 포함되지 않습니다.
이 함정은 특히 HTML 요소를 확장한 컴포넌트를 만들 때 문제가 됩니다. 컴포넌트가 ref를 받아야 하는데 ComponentProps만으로는 ref 타입을 정의할 수 없기 때문에 별도로 추가해야 합니다.
// ❌ ComponentProps<'input'>에 ref 미포함
const MyInput: React.FC<ComponentProps<'input'>> = (props) => {
return <input {...props} ref={someRef} />; // 타입 에러
};
// ✅ ref 타입 명시적 추가
type InputProps = ComponentProps<'input'> & {
ref?: React.Ref<HTMLInputElement>;
};ChangeEvent의 함정
React의 ChangeEvent는 제네릭 타입을 사용하기 때문에, 이벤트가 발생한 요소의 타입을 명시적으로 지정해야 합니다. 이를 생략하면 TypeScript가 가장 일반적인 HTMLElement 타입으로 추론해서, 폼 요소 특유의 속성에 접근할 수 없게 됩니다.
예를 들어 onChange={(e) => console.log(e.target.value)}처럼 제네릭 파라미터 없이 사용하면, target이 HTMLElement로 타입화되어 value 속성이 존재하지 않는다고 에러가 발생합니다.
특히 input, textarea, select 같은 폼 요소를 다룰 때는 ChangeEvent<HTMLInputElement>처럼 구체적인 요소 타입을 지정해야 target.value, target.checked 같은 속성에 안전하게 접근할 수 있습니다.
// ❌ 제네릭 타입 파라미터 생략
const handleChange = (e: React.ChangeEvent) => {
e.target.value; // 타입 에러
};
// ✅ 요소 타입 명시
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.target.value; // 정상 작동
};MouseEvent와 KeyboardEvent의 함정
MouseEvent와 KeyboardEvent 같은 다른 이벤트 타입들도 마찬가지로 제네릭 타입 파라미터를 명시적으로 지정해야 합니다. 생략하면 이벤트 객체의 속성들에 안전하게 접근할 수 없습니다.
특히 KeyboardEvent의 경우 key, code, altKey 같은 키보드 관련 속성들이 중요하며, MouseEvent의 경우 clientX, clientY, button 같은 마우스 관련 속성들이 필수적입니다.
// ❌ 제네릭 타입 파라미터 생략
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') { // 타입 에러: key 속성 접근 불가
// ...
}
};
// ✅ 요소 타입 명시
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { // 정상 작동
// ...
}
};예상 질문(FAQ)
Q. 왜 ReactNode를 써야 하나요? 그냥 ReactElement로 하면 안 되나요?
ReactNode는 렌더링 가능한 모든 것을 포함하는 가장 넓은 타입입니다. ReactElement는 JSX 엘리먼트만 허용하지만, ReactNode는 문자열, 숫자, null, undefined까지 포함합니다. children으로 다양한 콘텐츠를 허용하고 싶을 때는 ReactNode가 더 유연합니다. JavaScript로만 개발할 때는 이런 제약이 런타임에 발견되지만, TypeScript에서는 컴포넌트 설계 시점에 잡아줍니다.
Q. ComponentProps에 input 요소를 전달하면 어떤 타입이 되나요?
ComponentProps는 특정 컴포넌트나 HTML 요소의 props 타입을 추출하는 유틸리티 타입입니다. ComponentProps에 input 요소를 제네릭 파라미터로 전달하면 HTMLInputElement의 모든 속성을 포함하는 타입이 됩니다. 이를 활용하면 HTML 요소의 속성을 완전히 상속하면서 추가적인 props를 정의할 수 있습니다.
Q. ChangeEvent에 input 요소 타입을 전달하면 어떤 타입이 되나요?
ChangeEvent는 폼 요소의 값 변경 이벤트를 위한 특화된 타입입니다. 제네릭 타입 파라미터로 요소 타입을 지정할 수 있어(ChangeEvent<HTMLInputElement>), 이벤트 객체의 target 속성이 특정 HTML 요소 타입임을 보장합니다. 이를 통해 이벤트 핸들러에서 target.value 같은 속성에 타입 안전하게 접근할 수 있습니다.
Q. MouseEvent나 KeyboardEvent 같은 다른 이벤트 타입들은 어떻게 사용하나요?
MouseEvent나 KeyboardEvent 같은 이벤트 타입들도 ChangeEvent와 유사하게 제네릭 타입 파라미터를 받습니다. 예를 들어 MouseEvent<HTMLButtonElement>는 버튼 요소에서 발생하는 마우스 이벤트를, KeyboardEvent<HTMLInputElement>는 입력 요소에서 발생하는 키보드 이벤트를 타입화합니다. 각 이벤트 타입은 해당 이벤트의 고유한 속성들(마우스 좌표, 키 코드 등)에 안전하게 접근할 수 있게 해줍니다.
요약
React의 타입들은 JavaScript로만 개발할 때 겪는 많은 문제를 해결해 줍니다. ReactNode와 ReactElement로 렌더링 가능한 값들의 타입 안전성을 확보하고, PropsWithChildren과 ComponentProps로 컴포넌트 인터페이스를 체계적으로 정의할 수 있습니다.
특히 ChangeEvent, MouseEvent, KeyboardEvent 같은 이벤트 타입들로 다양한 이벤트 처리의 타입 안전성을 확보하고, 명시적 타입 지정으로 이벤트 핸들러의 안정성을 높이면, 런타임 에러의 상당 부분을 컴파일 타임에 잡아낼 수 있습니다.
이번 편에서는 React의 다양한 타입 중 가장 실용적이고 많이 사용되는 타입들을 중심으로 소개했습니다. 실제 프로젝트에서는 이 외에도 더 많은 타입들이 있지만, 여기서 다룬 타입들만으로도 충분히 안전한 React 컴포넌트 개발이 가능합니다.
참조
- React 공식 - 타입스크립트 사용하기
- React TypeScript Cheatsheets
- JSX.IntrinsicElements
- DefinitelyTyped