훅과 상태 관리
React 훅을 TypeScript와 함께 사용하면 상태 관리의 타입 안정성을 크게 향상시킬 수 있습니다.
이 절에서는 다양한 훅의 타입스크립트 사용법과 Best Practices를 다룹니다.
기본 훅 사용하기
- useState
import React, { useState } from 'react';
const Counter: React.FC = () => {
const [count, setCount] = useState<number>(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(prevCount => prevCount + 1)}>Increment</button>
</div>
);
};
- useEffect
import React, { useState, useEffect } from 'react';
const DataFetcher: React.FC = () => {
const [data, setData] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
const result = await response.text();
setData(result);
};
fetchData();
}, []);
return <div>{data ? data : 'Loading...'}</div>;
};
- useContext
import React, { createContext, useContext, useState } from 'react';
interface ThemeContextType {
isDark: boolean;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
const ThemeProvider: React.FC = ({ children }) => {
const [isDark, setIsDark] = useState(false);
const toggleTheme = () => setIsDark(!isDark);
return (
<ThemeContext.Provider value={{ isDark, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
const ThemedComponent: React.FC = () => {
const { isDark, toggleTheme } = useTheme();
return (
<div style={{ background: isDark ? 'black' : 'white', color: isDark ? 'white' : 'black' }}>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
};
커스텀 훅 작성하기
타입 안전한 커스텀 훅 예시
import { useState, useCallback } from 'react';
interface UseCounterReturn {
count: number;
increment: () => void;
decrement: () => void;
}
const useCounter = (initialValue: number = 0): UseCounterReturn => {
const [count, setCount] = useState<number>(initialValue);
const increment = useCallback(() => setCount(prev => prev + 1), []);
const decrement = useCallback(() => setCount(prev => prev - 1), []);
return { count, increment, decrement };
};
// 사용
const CounterComponent: React.FC = () => {
const { count, increment, decrement } = useCounter(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
};
useReducer와 타입스크립트
복잡한 상태 로직 관리
import React, { useReducer } from 'react';
type State = { count: number };
type Action = { type: 'increment' | 'decrement' | 'reset'; payload?: number };
const initialState: State = { count: 0 };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: action.payload ?? 0 };
default:
return state;
}
};
const Counter: React.FC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>Reset</button>
</div>
);
};
제네릭 훅
유연하고 재사용 가능한 훅 만들기
import { useState, useCallback } from 'react';
const useArray = <T,>(initialArray: T[] = []) => {
const [array, setArray] = useState<T[]>(initialArray);
const push = useCallback((element: T) => {
setArray(a => [...a, element]);
}, []);
const remove = useCallback((index: number) => {
setArray(a => a.filter((_, i) => i !== index));
}, []);
return { array, push, remove };
};
// 사용
const StringArrayComponent: React.FC = () => {
const { array, push, remove } = useArray<string>(['a', 'b', 'c']);
return (
<div>
{array.map((item, index) => (
<div key={index}>
{item} <button onClick={() => remove(index)}>Remove</button>
</div>
))}
<button onClick={() => push('new item')}>Add Item</button>
</div>
);
};
타입 세이프티 전역 상태 관리
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
interface State {
user: string | null;
isAuthenticated: boolean;
}
type Action =
| { type: 'LOGIN'; payload: string }
| { type: 'LOGOUT' };
const initialState: State = {
user: null,
isAuthenticated: false,
};
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'LOGIN':
return { user: action.payload, isAuthenticated: true };
case 'LOGOUT':
return { user: null, isAuthenticated: false };
default:
return state;
}
};
const AuthContext = createContext<{
state: State;
dispatch: React.Dispatch<Action>;
} | undefined>(undefined);
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AuthContext.Provider value={{ state, dispatch }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
비동기 작업 처리
타입 안전한 비동기 작업 처리
import { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
}
const useUser = (userId: number) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const data: User = await response.json();
setUser(data);
} catch (err) {
setError(err instanceof Error ? err : new Error('An unknown error occurred'));
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
return { user, loading, error };
};
의존성 배열과 타입 추론
의존성 배열에 대한 타입 추론을 개선하기 위해 useCallback
과 useMemo
를 사용할 때 명시적 타입을 제공할 수 있습니다.
const memoizedValue = useMemo<number>(() => {
return expensiveComputation(a, b);
}, [a, b]);
const memoizedCallback = useCallback<(value: string) => void>((value) => {
console.log(value);
}, []);
Best Practices와 가이드라인
- 명시적 타입 사용 : 가능한 한
any
타입 사용을 피하고 구체적인 타입을 사용하세요. - 널리 사용되는 타입 정의 : 자주 사용되는 타입은 별도의 파일로 분리하여 재사용성을 높이세요.
- 제네릭 활용 : 재사용 가능한 훅을 만들 때 제네릭을 활용하세요.
- 타입 가드 사용 : 조건부 렌더링 시 타입 가드를 사용하여 타입 안정성을 높이세요.
- 의존성 배열 타입 체크 : useEffect, useMemo, useCallback의 의존성 배열을 주의 깊게 관리하세요.
- 에러 처리 : 비동기 작업 시 적절한 에러 처리와 타입 지정을 하세요.
- Context 사용 시 타입 안정성 : Context 생성 시 명시적 타입을 제공하고, 사용 시 타입 체크를 수행하세요.
- 불변성 유지 : 상태 업데이트 시 불변성을 유지하고, 필요한 경우 readonly 타입을 사용하세요.
- 테스트 작성 : 타입스크립트로 작성된 훅에 대해 단위 테스트를 작성하세요.
- IDE 지원 활용 : VSCode 등 IDE의 타입스크립트 지원 기능을 최대한 활용하세요.
React 훅을 TypeScript와 함께 사용하면 런타임 오류를 줄이고 코드의 자기 문서화를 개선할 수 있습니다.