React 요약 14 - 타입스크립트와 React
Updated:
기본 타입 정의
// 기본 타입 예제
let isDone: boolean = false;
let decimal: number = 6;
let color: string = "blue";
let list: number[] = [1, 2, 3];
let tuple: [string, number] = ["hello", 10];
let anyValue: any = 4;
// 열거형
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT"
}
// 함수 타입
function add(x: number, y: number): number {
return x + y;
}
// 선택적 매개변수와 기본값
function buildName(firstName: string, lastName?: string): string {
return lastName ? `${firstName} ${lastName}` : firstName;
}
// 함수 오버로드
function getLength(value: string): number;
function getLength(value: any[]): number;
function getLength(value: string | any[]): number {
return value.length;
}
// 인터페이스
interface User {
id: number;
name: string;
email: string;
age?: number; // 선택적 속성
readonly createdAt: Date; // 읽기 전용 속성
}
// 타입 별칭
type ID = string | number;
type UserWithID = User & { id: ID };
// 유니온 타입과 교차 타입
type Result = "success" | "failure";
type UserOrAdmin = User | { role: string; permissions: string[] };
type EnhancedUser = User & { role: string };
// 제네릭
function identity<T>(arg: T): T {
return arg;
}
// 제네릭 인터페이스
interface Box<T> {
value: T;
}
// 제네릭 클래스
class Queue<T> {
private data: T[] = [];
push(item: T): void {
this.data.push(item);
}
pop(): T | undefined {
return this.data.shift();
}
}
// 유틸리티 타입 예제
interface Todo {
title: string;
description: string;
completed: boolean;
}
// Partial: 모든 속성을 선택적으로 만듦
type PartialTodo = Partial<Todo>;
// Required: 모든 속성을 필수로 만듦
type RequiredTodo = Required<Todo>;
// Pick: 특정 속성만 선택
type TodoPreview = Pick<Todo, "title" | "completed">;
// Omit: 특정 속성을 제외
type TodoWithoutDescription = Omit<Todo, "description">;
// Record: 키-값 쌍의 타입 정의
type TodoRecord = Record<string, Todo>;
코멘트: TypeScript는 JavaScript에 정적 타입 시스템을 추가한 언어로, 코드 품질과 개발자 경험을 향상시킵니다. 주요 타입 기능은 다음과 같습니다:
- 기본 타입: boolean, number, string, array, tuple, any, void 등
- 인터페이스: 객체 구조를 정의하는 강력한 방법
- 타입 별칭: 기존 타입에 새 이름을 부여하거나 복잡한 타입 정의
유니온과 교차 타입: 타입 조합을 위한 OR( )와 AND(&) 연산 - 제네릭: 재사용 가능한 컴포넌트를 다양한 타입에 대해 작업할 수 있게 함
- 유틸리티 타입: 기존 타입을 변환하는 내장 도구 (Partial, Required, Pick 등)
TypeScript를 사용하면 컴파일 시점에 오류를 발견하고, 코드 자동 완성과 문서화 혜택을 얻을 수 있습니다.
React 컴포넌트 타입 정의
import React, { useState, useEffect, useRef, ReactNode } from 'react';
// 함수형 컴포넌트의 props 타입 정의
interface ButtonProps {
text: string;
onClick: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
className?: string;
children?: ReactNode;
}
// 함수형 컴포넌트
const Button: React.FC<ButtonProps> = ({
text,
onClick,
disabled = false,
variant = 'primary',
size = 'medium',
className = '',
children
}) => {
return (
<button
onClick={onClick}
disabled={disabled}
className={`button button-${variant} button-${size} ${className}`}
>
{text}
{children}
</button>
);
};
// 제네릭 컴포넌트
interface SelectProps<T> {
items: T[];
selectedItem: T | null;
onSelect: (item: T) => void;
renderItem: (item: T) => ReactNode;
getKey: (item: T) => string | number;
}
function Select<T>({
items,
selectedItem,
onSelect,
renderItem,
getKey
}: SelectProps<T>) {
return (
<ul className="select">
{items.map(item => (
<li
key={getKey(item)}
className={item === selectedItem ? 'selected' : ''}
onClick={() => onSelect(item)}
>
{renderItem(item)}
</li>
))}
</ul>
);
}
// 상태와 이벤트 핸들러 타입
interface User {
id: number;
name: string;
email: string;
}
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// useRef 타입
const prevUserIdRef = useRef<number>();
// 이벤트 핸들러 타입
const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log('Button clicked', event.currentTarget);
};
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log('Input changed', event.target.value);
};
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log('Form submitted');
};
// useEffect
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
// API 호출 시뮬레이션
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.message : 'An unknown error occurred');
} finally {
setLoading(false);
}
};
fetchUser();
// 이전 userId 저장
prevUserIdRef.current = userId;
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div className="user-profile">
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
<form onSubmit={handleFormSubmit}>
<input
type="text"
value={user.name}
onChange={handleInputChange}
/>
<button type="submit" onClick={handleButtonClick}>
Update Profile
</button>
</form>
</div>
);
};
// 클래스 컴포넌트 타입
interface CounterProps {
initialCount: number;
step?: number;
}
interface CounterState {
count: number;
lastUpdated: Date | null;
}
class Counter extends React.Component<CounterProps, CounterState> {
// 기본 props 정의
static defaultProps: Partial<CounterProps> = {
step: 1
};
constructor(props: CounterProps) {
super(props);
this.state = {
count: props.initialCount,
lastUpdated: null
};
}
increment = () => {
this.setState(prevState => ({
count: prevState.count + (this.props.step || 1),
lastUpdated: new Date()
}));
};
decrement = () => {
this.setState(prevState => ({
count: prevState.count - (this.props.step || 1),
lastUpdated: new Date()
}));
};
render() {
return (
<div>
<h2>Count: {this.state.count}</h2>
{this.state.lastUpdated && (
<p>Last updated: {this.state.lastUpdated.toLocaleTimeString()}</p>
)}
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
</div>
);
}
}
// 고차 컴포넌트(HOC) 타입
interface WithLoadingProps {
loading: boolean;
}
// HOC 함수
function withLoading<P extends object>(
Component: React.ComponentType<P>
): React.FC<P & WithLoadingProps> {
return ({ loading, ...props }: WithLoadingProps & P) => {
if (loading) return <div>Loading...</div>;
return <Component {...props as P} />;
};
}
// HOC 사용
const UserListWithLoading = withLoading(
({ users }: { users: User[] }) => (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
);
// 사용 예시
const App: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
// 데이터 로딩 시뮬레이션
setTimeout(() => {
setUsers([
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
]);
setLoading(false);
}, 2000);
}, []);
return <UserListWithLoading users={users} loading={loading} />;
};
// 컨텍스트 API 타입
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
// 기본값 생성 (타입 체크를 위한 더미 함수 포함)
const defaultThemeContext: ThemeContextType = {
theme: 'light',
toggleTheme: () => {}
};
const ThemeContext = React.createContext<ThemeContextType>(defaultThemeContext);
const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
const value = { theme, toggleTheme };
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};
// 컨텍스트 사용
const ThemedButton: React.FC<{ text: string }> = ({ text }) => {
const { theme, toggleTheme } = React.useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff',
border: `1px solid ${theme === 'light' ? '#333' : '#fff'}`
}}
>
{text} (Theme: {theme})
</button>
);
};
// 폼 이벤트 처리
interface LoginFormState {
email: string;
password: string;
}
const LoginForm: React.FC<{ onSubmit: (data: LoginFormState) => void }> = ({ onSubmit }) => {
const [formData, setFormData] = useState<LoginFormState>({
email: '',
password: ''
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit(formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
required
/>
</div>
<button type="submit">Login</button>
</form>
);
};
// 타입스크립트와 React Hooks
function useCounter(initialValue: number = 0, step: number = 1) {
const [count, setCount] = useState<number>(initialValue);
const increment = () => setCount(c => c + step);
const decrement = () => setCount(c => c - step);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// 제네릭 커스텀 Hook
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value: T) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// 타입스크립트와 React Router
interface RouteParams {
id: string;
}
interface UserDetailProps {
match: {
params: RouteParams;
};
}
const UserDetail: React.FC<UserDetailProps> = ({ match }) => {
const userId = parseInt(match.params.id, 10);
// 사용자 데이터 로딩 로직...
return <div>User ID: {userId}</div>;
};
// 타입스크립트와 Redux
interface AppState {
counter: {
value: number;
};
user: {
data: User | null;
loading: boolean;
error: string | null;
};
}
// 액션 타입
enum ActionType {
INCREMENT = 'INCREMENT',
DECREMENT = 'DECREMENT',
FETCH_USER_REQUEST = 'FETCH_USER_REQUEST',
FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS',
FETCH_USER_FAILURE = 'FETCH_USER_FAILURE'
}
// 액션 인터페이스
interface IncrementAction {
type: ActionType.INCREMENT;
payload: number;
}
interface DecrementAction {
type: ActionType.DECREMENT;
payload: number;
}
interface FetchUserRequestAction {
type: ActionType.FETCH_USER_REQUEST;
}
interface FetchUserSuccessAction {
type: ActionType.FETCH_USER_SUCCESS;
payload: User;
}
interface FetchUserFailureAction {
type: ActionType.FETCH_USER_FAILURE;
payload: string;
}
// 유니온 타입으로 모든 액션 타입 정의
type CounterAction = IncrementAction | DecrementAction;
type UserAction = FetchUserRequestAction | FetchUserSuccessAction | FetchUserFailureAction;
type AppAction = CounterAction | UserAction;
// 액션 생성자
const increment = (amount: number): IncrementAction => ({
type: ActionType.INCREMENT,
payload: amount
});
const decrement = (amount: number): DecrementAction => ({
type: ActionType.DECREMENT,
payload: amount
});
// 리듀서
const counterReducer = (state = { value: 0 }, action: CounterAction) => {
switch (action.type) {
case ActionType.INCREMENT:
return { ...state, value: state.value + action.payload };
case ActionType.DECREMENT:
return { ...state, value: state.value - action.payload };
default:
return state;
}
};
// 타입스크립트와 styled-components
import styled from 'styled-components';
interface ButtonStyleProps {
primary?: boolean;
size?: 'small' | 'medium' | 'large';
outlined?: boolean;
}
const StyledButton = styled.button<ButtonStyleProps>`
background-color: ${props => (props.primary ? '#0070f3' : '#fff')};
color: ${props => (props.primary ? '#fff' : '#0070f3')};
border: ${props => (props.outlined ? '1px solid #0070f3' : 'none')};
padding: ${props => {
switch (props.size) {
case 'small':
return '8px 16px';
case 'large':
return '16px 32px';
default:
return '12px 24px';
}
}};
border-radius: 4px;
font-size: ${props => (props.size === 'large' ? '18px' : '16px')};
cursor: pointer;
&:hover {
opacity: 0.8;
}
`;
// 사용 예시
const StyledButtonExample: React.FC = () => {
return (
<div>
<StyledButton>Default Button</StyledButton>
<StyledButton primary>Primary Button</StyledButton>
<StyledButton size="large" outlined>
Large Outlined Button
</StyledButton>
</div>
);
};
코멘트: TypeScript와 React를 함께 사용하면 타입 안전성과 개발자 경험이 크게 향상됩니다. 주요 패턴은 다음과 같습니다:
- 컴포넌트 Props 타입 정의:
- 인터페이스나 타입 별칭을 사용하여 props 타입 정의
- 선택적 속성(?)과 기본값 활용
React.FC<Props>
또는 함수 매개변수에 직접 타입 지정- 이벤트 처리:
React.MouseEvent
,React.ChangeEvent
등의 제네릭 타입 사용- 이벤트 핸들러 함수에 적절한 타입 지정
- Hooks 타입 지정:
useState<T>
,useRef<T>
등에 제네릭 타입 사용- 커스텀 Hook에 타입 안전성 추가
- 컨텍스트 API:
- 컨텍스트 값과 Provider에 타입 지정
- 기본값에 더미 함수 포함하여 타입 체크 지원
- 고급 패턴:
- 제네릭 컴포넌트로 재사용성 높이기
- 고차 컴포넌트(HOC)에 타입 지정
- 조건부 타입과 유틸리티 타입 활용
TypeScript를 사용하면 컴파일 시점에 많은 오류를 잡을 수 있고, 자동 완성과 타입 추론을 통해 개발 생산성이 향상됩니다.
Leave a comment