最終回では、Reactのコードをより堅牢で予測しやすくする強力なツール、
TypeScript
をReactプロジェクトで使いこなすためのヒントを紹介します。
TypeScriptはJavaScriptに静的型を加え、コードを書く段階、つまりコンパイル時にエラーを発見できます。コード補完やリファクタリングもしやすくなるため、開発の生産性が大きく向上します。今ではReactとTypeScriptを組み合わせるのはごく一般的です。
型を付けるだけでなく、ジェネリクスで再利用可能なコンポーネントを作ったり、カスタムフックの型推論を活用したり、フォーム処理の型安全性を高めたりすることで、TypeScriptの強みをさらに引き出せます。今回はこうした応用を中心に、TypeScriptとReactを組み合わせた開発を一歩進めます。
コンポーネントで複数の型のデータを扱いたいことがあります。例えば、数値の配列を一覧にするコンポーネントと、文字列の配列を一覧にするコンポーネントは、それぞれ作る必要があるのでしょうか。
このようなとき、*ジェネリクス(Generics)*を使えば、型を変数のように扱い、さまざまな型に対応する再利用可能なコンポーネントを作れます。
ジェネリクスは、型を定義時に固定せず、関数やコンポーネントを
使う時点
で決められる機能です。関数に引数を渡すのと同じように、通常は
T、
U、
K などで表す型変数を通じて型を受け取ります。
// 제네릭 함수 예시: 어떤 타입의 값이든 받아서 그대로 반환
function identity < T >( arg : T ) : T {
return arg;
}
let output1 = identity < string >( "hello" ); // T는 string 타입으로 결정됨
let output2 = identity < number >( 123 ); // T는 number 타입으로 결정됨
let output3 = identity ( "auto" ); // 타입 인자 생략 시 추론됨 (string)
Reactコンポーネントにジェネリクスを適用する 🔗
この考え方をReactの関数コンポーネントに適用しましょう。さまざまな型の要素の配列を受け取って一覧にする List コンポーネントを作ります。
import React from 'react' ;
// 제네릭 Props 타입 정의
// T라는 타입 변수를 받음 (사용 시 결정될 아이템의 타입)
interface ListProps < T > {
items : T []; // items 배열은 T 타입의 요소들로 구성
renderItem : ( item : T , index : number ) => React . ReactNode ; // 각 아이템을 렌더링하는 함수
// keyExtractor?: (item: T) => string | number; // key 추출 함수 (선택적)
}
// 제네릭 함수형 컴포넌트 정의
// <T,> 처럼 쉼표를 찍는 이유는 JSX 태그와 구분하기 위함 (또는 <T extends {}> 사용)
function List < T >({ items , renderItem } : ListProps < T >) : React . ReactElement {
return (
< ul >
{ items. map (( item , index ) => (
// key prop은 실제 사용 시 renderItem 내부나 여기서 적절히 제공해야 함
// 예: <li key={keyExtractor ? keyExtractor(item) : index}>
< React.Fragment key ={ index } > { /* key는 실제 상황에 맞게 수정 필요 */ }
{ renderItem (item, index) }
</ React.Fragment >
)) }
</ ul >
);
}
// --- 컴포넌트 사용 예시 ---
interface User { id : number ; name : string ; }
const users : User [] = [ { id: 1 , name: 'Alice' }, { id: 2 , name: 'Bob' } ];
interface Product { sku : string ; title : string ; price : number ; }
const products : Product [] = [
{ sku: 'P001' , title: '노트북' , price: 1500000 },
{ sku: 'P002' , title: '키보드' , price: 80000 },
];
function App () {
return (
< div >
< h2 >사용자 목록</ h2 >
{ /* T를 User 타입으로 지정하여 사용 */ }
< List < User >
items ={ users }
renderItem ={ ( user ) => < li key ={ user.id } > { user.name } </ li > }
/>
< h2 >상품 목록</ h2 >
{ /* T를 Product 타입으로 지정하여 사용 */ }
< List < Product >
items ={ products }
renderItem ={ ( product ) => (
< li key ={ product.sku } >
{ product.title } - { product.price. toLocaleString () } 원
</ li >
) }
/>
</ div >
);
}
export default App; // List 컴포넌트 export 필요 시 추가
この List はジェネリック型 T を使い、任意の型の配列(items: T[])を扱えます。renderItem propも T 型を引数に取るため、型ごとの描画処理を外から渡せます。User の一覧と Product の一覧を1つの List で扱えるので、コードの再利用性が大きく向上します。
以前は React.FC<ListProps<T>> のように React.FC を使う例も多くありました。近年は、React.FC の利用に伴ういくつかの難点(children propが暗黙に含まれることなど)から、上の例のように関数のシグネチャで直接型を指定する方法が好まれる傾向があります。
ジェネリックコンポーネントの概念図
9日目で学んだカスタムフックは、状態に関わるロジックを再利用するための強力な手段です。TypeScriptと併用すれば、入力(引数)と出力(戻り値)の型を明確にし、安全性を高められます。さらに、*型推論(Type Inference)*を使いこなすと、より便利で柔軟なフックを作れます。
まず、カスタムフックの引数と戻り値の型を明示する基本的な方法です。
import { useState } from 'react' ;
// 커스텀 훅: 입력 필드 값 관리
function useInput ( initialValue : string ) : [ string , ( e : React . ChangeEvent < HTMLInputElement >) => void , () => void ] {
const [ value , setValue ] = useState (initialValue);
const handleChange = ( e : React . ChangeEvent < HTMLInputElement >) => {
setValue (e.target.value);
};
const reset = () => {
setValue (initialValue);
};
// 반환 타입을 튜플(Tuple) 형태로 명시
return [value, handleChange, reset];
}
// 컴포넌트에서 사용
function MyForm () {
const [ name , handleNameChange , resetName ] = useInput ( '' );
// name은 string, handleNameChange는 함수, resetName은 함수 타입으로 자동 추론됨
// ...
}
戻り値の型を明記すると、フックの使い方がわかりやすくなります。
TypeScriptは関数本体の戻り値を分析して、戻り値の型を自動で推論できます。型が複雑でなかったり、明らかだったりする場合には、明示せずに推論に任せるとコードが簡潔になります。ただし、ライブラリとして公開する場合や複雑な場合には、型を明記するほうがよいこともあります。
// 반환 타입 명시 생략 (TypeScript가 추론하도록 함)
function useToggle ( initialValue : boolean = false ) { // 기본값 설정 가능
const [ value , setValue ] = useState (initialValue);
const toggle = () => setValue ( prev => ! prev);
// TypeScript는 반환 값을 보고 [boolean, () => void] 타입으로 추론함
return [value, toggle] as const ; // 'as const' 추가: 튜플 타입을 더 정확하게 추론하고 readonly로 만듦
}
function ToggleComponent () {
const [ isOn , toggleIsOn ] = useToggle ( false );
// isOn은 boolean, toggleIsOn은 () => void 타입으로 잘 추론됨
// ...
}
as const を使うと、返す配列を読み取り専用のタプル(readonly tuple)としてより正確に推論し、分割代入の際に各要素の型を固定できます。
カスタムフックにジェネリクスを使うと、複数の型に対応できる柔軟なフックを作れます。例えば、APIからデータを取る useFetch フックを考えます。レスポンスデータの型はAPIによって異なります。
import React, { useState, useEffect } from 'react' ;
import axios from 'axios' ;
// 제네릭 커스텀 훅 정의
// T는 예상되는 응답 데이터의 타입을 나타냄
function useFetch < T >( url : string | null ) {
const [ data , setData ] = useState < T | null >( null ); // 데이터 타입 T
const [ loading , setLoading ] = useState ( true );
const [ error , setError ] = useState < Error | null >( null );
useEffect (() => {
if ( ! url) { // url이 없으면 요청하지 않음
setLoading ( false );
return ;
}
const controller = new AbortController ();
setLoading ( true );
setData ( null ); // 이전 데이터 초기화
setError ( null );
axios. get < T >(url, { signal: controller.signal }) // 응답 데이터 타입을 T로 기대
. then ( response => {
setData (response.data);
})
. catch ( err => {
if ( ! axios. isCancel (err)) {
setError (err);
}
})
. finally (() => {
setLoading ( false );
});
return () => {
controller. abort ();
};
}, [url]); // url이 변경될 때마다 재요청
// 반환 값의 타입은 TypeScript가 { data: T | null, loading: boolean, error: Error | null } 로 추론
return { data, loading, error };
}
// --- 컴포넌트에서 사용 예시 ---
interface User { id : number ; name : string ; }
function UserComponent ({ userId } : { userId : number }) {
// useFetch 사용 시 제네릭 타입(User) 지정
const { data : user , loading , error } = useFetch < User >( `/api/users/${ userId }` );
if (loading) return < div >로딩 중...</ div >;
if (error) return < div >오류: { error.message } </ div >;
// 여기서 user 타입은 User | null 로 정확하게 추론됨
return < h1 > { user?.name } </ h1 >;
}
interface Product { sku : string ; title : string ; }
function ProductComponent ({ productId } : { productId : string }) {
// Product 타입 지정
const { data : product , loading , error } = useFetch < Product >( `/api/products/${ productId }` );
// ... product 타입 사용 ...
}
ジェネリクスを使えば、1つの useFetch でさまざまな型のAPIレスポンスを扱えます。呼び出し側でも、データの型ごとの型安全性が得られます。
カスタムフックで型を推論する流れ
Reactでフォームを扱うとき、入力の妥当性を検証し、データの型を安全に管理することは重要ですが、手間がかかります。ユーザーの入力値はもともと文字列であることが多く、数値などの別の型に変換したり、メールアドレスやURLなどの形式を検証したりすると、型に関するエラーが起きやすくなります。スキーマベースの検証ライブラリである
Zod
や
Yup
を使うと、型安全性とバリデーションをまとめて扱えます。
Zod / Yup
は、データの構造や制約、つまりスキーマ(Schema)をコードで定義できるライブラリです。型、必須かどうか、最小・最大の長さ、メールアドレス形式などを指定できます。
主な機能
定義したスキーマに沿って、データの妥当性を簡単に検証できます。
スキーマからTypeScriptの型を自動で推論できるため、型定義と検証ロジックを1か所で管理し、コードの重複を減らせます。
React Hook FormとZodを組み合わせる 🔗
フォーム管理ライブラリの
React Hook Form
と併用すると、両方の利点を活かせます。
@hookform/resolvers パッケージを使うと、ZodやYupのスキーマをReact Hook Formの検証ルールとして簡単に組み込めます。
npm install react-hook-form zod @hookform/resolvers
# 또는
yarn add react-hook-form zod @hookform/resolvers
import { z } from 'zod' ; // zod 임포트
// 폼 데이터 스키마 정의 (유효성 규칙 포함)
const SignUpSchema = z. object ({
email: z. string (). min ( 1 , "이메일은 필수 항목입니다." ). email ( "유효한 이메일 주소를 입력해주세요." ),
password: z. string (). min ( 8 , "비밀번호는 최소 8자 이상이어야 합니다." ),
confirmPassword: z. string (). min ( 1 , "비밀번호 확인은 필수 항목입니다." ),
age: z. number (). min ( 18 , "만 18세 이상만 가입 가능합니다." ). optional (), // 선택적 숫자 필드
}). refine ( data => data.password === data.confirmPassword, { // 두 필드 비교 검증
message: "비밀번호가 일치하지 않습니다." ,
path: [ "confirmPassword" ], // 오류 메시지를 표시할 필드 지정
});
// 스키마로부터 TypeScript 타입 자동 추론!
type SignUpFormValues = z . infer < typeof SignUpSchema>;
Zodのスキーマを定義すれば、z.infer でその構造に対応するTypeScriptの型を取得できます。interface や type を別途定義する必要はありません。
3. React Hook Formに適用する 🔗
import React from 'react' ;
import { useForm, SubmitHandler } from 'react-hook-form' ;
import { zodResolver } from '@hookform/resolvers/zod' ; // zod 리졸버 임포트
import { z } from 'zod' ; // zod 스키마 정의는 다른 파일에 있어도 됨
// 스키마 및 타입 정의 (다른 파일에서 가져왔다고 가정)
const SignUpSchema = z. object ({ /* ... 스키마 정의 ... */ });
type SignUpFormValues = z . infer < typeof SignUpSchema>;
function SignUpForm () {
const {
register ,
handleSubmit ,
formState : { errors } // errors 객체를 통해 유효성 검사 오류 확인 가능
} = useForm < SignUpFormValues >({ // useForm에 추론된 타입 전달
resolver: zodResolver (SignUpSchema), // zod 스키마를 resolver로 사용
// defaultValues: { email: '', password: '', confirmPassword: '', age: undefined } // 기본값 설정
});
// 폼 제출 시 실행될 핸들러 (타입 안전성 보장!)
// data 매개변수는 SignUpFormValues 타입으로 자동 추론됨
const onSubmit : SubmitHandler < SignUpFormValues > = ( data ) => {
console. log (data);
// data.email, data.password 등 타입이 보장된 상태로 사용 가능
// data.age는 number | undefined 타입
};
return (
< form onSubmit ={ handleSubmit (onSubmit) } >
< div >
< label htmlFor = "email" >이메일</ label >
< input id = "email" type = "email" {... register ( 'email' ) } />
{ errors.email && < p style ={ { color: 'red' } } > { errors.email.message } </ p > }
</ div >
< div >
< label htmlFor = "password" >비밀번호</ label >
< input id = "password" type = "password" {... register ( 'password' ) } />
{ errors.password && < p style ={ { color: 'red' } } > { errors.password.message } </ p > }
</ div >
< div >
< label htmlFor = "confirmPassword" >비밀번호 확인</ label >
< input id = "confirmPassword" type = "password" {... register ( 'confirmPassword' ) } />
{ errors.confirmPassword && < p style ={ { color: 'red' } } > { errors.confirmPassword.message } </ p > }
</ div >
< div >
< label htmlFor = "age" >나이 (선택)</ label >
{ /* register의 두 번째 인자로 valueAsNumber: true 전달 시 숫자로 변환 시도 */ }
< input id = "age" type = "number" {... register ( 'age' , { valueAsNumber: true }) } />
{ errors.age && < p style ={ { color: 'red' } } > { errors.age.message } </ p > }
</ div >
< button type = "submit" >가입하기</ button >
</ form >
);
}
このようにZodやYupをReact Hook Formと組み合わせると、次のメリットがあります。
検証ルールと型を、スキーマ定義の1か所で管理できます。
useForm と handleSubmit の data オブジェクトの型が自動で推論され、型安全性が大きく向上します。
検証ロジックを自分で書かず、スキーマに委ねられるため、コードが簡潔になります。
errors オブジェクトから、各フィールドのエラーメッセージを簡単に表示できます。
フォーム送信の流れ
今回は、TypeScriptとReactを組み合わせて開発体験とコード品質を高めるヒントやパターンを学びました。
**ジェネリクス(Generics)**で型を変数のように扱うと、さまざまな型に対応した再利用可能で型安全なコンポーネントを作れます。
カスタムフック
では、TypeScriptの型推論を活用し、必要に応じてジェネリクスを使うと、柔軟性と安全性を両立できます。
ZodやYup
などのスキーマベースの検証ライブラリを使うと、フォームデータの検証と型定義をまとめて管理できます。React Hook Formと連携させることで、フォーム処理の型安全性
を大きく高められます。
TypeScriptは、エラーの防止に加えて、コードの意図を明確にし、リファクタリングを容易にし、よりよい設計を促す強力なツールです。ReactとともにTypeScriptのさまざまな機能を活用し、堅牢で保守しやすいアプリケーションを作っていきましょう。
👨💻
これで「Reactを理解して使う」シリーズの最後のテーマまで扱いました。ここまでお付き合いいただき、ありがとうございました。このシリーズがReact開発の道しるべになれば幸いです。