React와 타입스크립트: 컴포넌트 props 타입 정의 📚
안녕하세요, 여러분! 오늘은 React와 TypeScript를 함께 사용할 때 가장 중요한 주제 중 하나인 "컴포넌트 props 타입 정의"에 대해 깊이 있게 알아보겠습니다. 이 글은 재능넷의 "지식인의 숲" 메뉴에 등록될 예정이며, 프로그램 개발 카테고리의 TypeScript 섹션에 속합니다. 🌳
React와 TypeScript의 조합은 현대 웹 개발에서 매우 강력하고 인기 있는 스택입니다. TypeScript를 사용함으로써 우리는 JavaScript에 정적 타입을 추가하여 코드의 안정성과 가독성을 크게 향상시킬 수 있습니다. 특히 React 컴포넌트의 props에 타입을 정의하는 것은 매우 중요한 작업인데, 이를 통해 우리는 컴포넌트 간의 데이터 흐름을 명확히 하고 잠재적인 버그를 사전에 방지할 수 있기 때문입니다.
이 글에서는 React 컴포넌트의 props에 TypeScript 타입을 정의하는 방법부터 시작하여, 고급 기법과 모범 사례까지 상세히 다룰 예정입니다. 초보자부터 경험 많은 개발자까지 모두에게 유용한 정보를 제공할 것이며, 실제 프로젝트에 바로 적용할 수 있는 실용적인 팁들도 함께 소개하겠습니다.
자, 그럼 React와 TypeScript의 세계로 깊이 들어가 봅시다! 🚀
1. TypeScript와 React: 기본 개념 🏗️
TypeScript와 React를 함께 사용하기 전에, 먼저 각각의 기본 개념을 간단히 살펴보겠습니다.
1.1 TypeScript란?
TypeScript는 Microsoft에서 개발한 JavaScript의 상위 집합(superset) 언어입니다. 주요 특징은 다음과 같습니다:
- 정적 타입 지원
- 객체 지향 프로그래밍 기능
- 컴파일 시점 오류 검출
- 강력한 개발 도구 지원
TypeScript는 대규모 애플리케이션 개발에 특히 유용하며, 코드의 품질과 유지보수성을 크게 향상시킵니다.
1.2 React란?
React는 Facebook에서 개발한 JavaScript 라이브러리로, 사용자 인터페이스를 구축하기 위해 사용됩니다. 주요 특징은 다음과 같습니다:
- 컴포넌트 기반 아키텍처
- 가상 DOM을 통한 효율적인 렌더링
- 단방향 데이터 흐름
- JSX를 통한 선언적 UI 설계
React는 현대 웹 개발에서 가장 인기 있는 프론트엔드 라이브러리 중 하나입니다.
1.3 TypeScript와 React의 조합
TypeScript와 React를 함께 사용하면 다음과 같은 이점을 얻을 수 있습니다:
- 컴포넌트 props와 state에 대한 타입 안정성
- 더 나은 자동 완성과 IntelliSense 지원
- 리팩토링 용이성 향상
- 런타임 오류 감소
이러한 이점들로 인해 많은 개발자들이 React 프로젝트에 TypeScript를 도입하고 있습니다.
이제 TypeScript와 React의 기본 개념을 이해했으니, 다음 섹션에서는 React 컴포넌트에서 props의 타입을 정의하는 방법에 대해 자세히 알아보겠습니다.
2. React 컴포넌트에서 Props 타입 정의하기 🧩
React 컴포넌트에서 props의 타입을 정의하는 것은 TypeScript를 사용할 때 가장 기본적이면서도 중요한 작업입니다. 이를 통해 우리는 컴포넌트가 어떤 props를 받을 수 있는지 명확히 할 수 있고, 잘못된 타입의 props가 전달되는 것을 방지할 수 있습니다.
2.1 기본적인 Props 타입 정의
가장 간단한 형태의 props 타입 정의는 다음과 같습니다:
interface GreetingProps {
name: string;
}
const Greeting: React.FC<GreetingProps> = ({ name }) => {
return <h1>Hello, {name}!</h1>;
};
여기서 GreetingProps
인터페이스는 name
이라는 문자열 타입의 prop을 정의합니다. React.FC
는 "Function Component"의 약자로, 함수형 컴포넌트의 타입을 나타냅니다.
2.2 선택적 Props 정의하기
모든 props가 항상 필수는 아닙니다. 선택적 props는 물음표(?)를 사용하여 정의할 수 있습니다:
interface UserProps {
name: string;
age?: number; // 선택적 prop
}
const User: React.FC<UserProps> = ({ name, age }) => {
return (
<div>
<p>Name: {name}</p>
{age && <p>Age: {age}</p>}
</div>
);
};
이 예제에서 age
는 선택적 prop입니다. 컴포넌트를 사용할 때 age
를 제공하지 않아도 TypeScript 컴파일러는 오류를 발생시키지 않습니다.
2.3 함수 Props 타입 정의하기
컴포넌트에 함수를 prop으로 전달하는 경우도 많습니다. 이런 경우 함수의 시그니처를 정확히 정의하는 것이 중요합니다:
interface ButtonProps {
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
label: string;
}
const Button: React.FC<ButtonProps> = ({ onClick, label }) => {
return <button onClick={onClick}>{label}</button>;
};
이 예제에서 onClick
prop은 React.MouseEvent
타입의 매개변수를 받고 아무것도 반환하지 않는 함수로 정의되었습니다.
2.4 children Props 타입 정의하기
React 컴포넌트는 종종 children
prop을 받아 자식 요소를 렌더링합니다. TypeScript에서는 이를 다음과 같이 정의할 수 있습니다:
interface ContainerProps {
children: React.ReactNode;
}
const Container: React.FC<ContainerProps> = ({ children }) => {
return <div className="container">{children}</div>;
};
React.ReactNode
타입은 React 요소, 문자열, 숫자, 배열 등 React에서 렌더링 가능한 모든 것을 포함합니다.
2.5 Union 타입을 사용한 Props 정의
때로는 prop이 여러 가지 타입 중 하나일 수 있습니다. 이런 경우 Union 타입을 사용할 수 있습니다:
interface AlertProps {
message: string;
type: 'info' | 'warning' | 'error';
}
const Alert: React.FC<AlertProps> = ({ message, type }) => {
return <div className={`alert alert-${type}`}>{message}</div>;
};
이 예제에서 type
prop은 'info', 'warning', 'error' 중 하나의 값만 가질 수 있습니다.
이러한 다양한 방법으로 props의 타입을 정의함으로써, 우리는 컴포넌트의 인터페이스를 명확히 하고 타입 안정성을 확보할 수 있습니다. 다음 섹션에서는 더 복잡한 props 타입 정의 방법과 고급 기법들을 살펴보겠습니다.
3. 고급 Props 타입 정의 기법 🚀
기본적인 props 타입 정의 방법을 살펴보았으니, 이제 더 복잡하고 고급스러운 타입 정의 기법들을 알아보겠습니다. 이러한 기법들은 더 복잡한 컴포넌트나 대규모 애플리케이션에서 유용하게 사용될 수 있습니다.
3.1 제네릭을 사용한 Props 타입 정의
제네릭을 사용하면 재사용 가능한 컴포넌트를 만들 수 있습니다. 예를 들어, 다양한 타입의 데이터를 표시할 수 있는 리스트 컴포넌트를 만들어 봅시다:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
);
}
// 사용 예:
<List
items={['Apple', 'Banana', 'Orange']}
renderItem={(item) => <span>{item}</span>}
/>
이 예제에서 List
컴포넌트는 어떤 타입의 배열이든 받을 수 있으며, 각 항목을 어떻게 렌더링할지 결정하는 함수도 prop으로 받습니다.
3.2 Intersection 타입을 사용한 Props 확장
때로는 기존의 props 타입을 확장하여 새로운 props 타입을 만들어야 할 때가 있습니다. 이럴 때 Intersection 타입을 사용할 수 있습니다:
interface BaseButtonProps {
onClick: () => void;
children: React.ReactNode;
}
interface PrimaryButtonProps extends BaseButtonProps {
variant: 'primary';
size: 'small' | 'medium' | 'large';
}
type ButtonProps = BaseButtonProps | PrimaryButtonProps;
const Button: React.FC<ButtonProps> = (props) => {
if ('variant' in props) {
// PrimaryButtonProps
return (
<button
onClick={props.onClick}
className={`btn-primary btn-${props.size}`}
>
{props.children}
</button>
);
}
// BaseButtonProps
return <button onClick={props.onClick}>{props.children}</button>;
};
이 예제에서는 기본 버튼 props를 확장하여 primary 버튼을 위한 추가적인 props를 정의했습니다.
3.3 Mapped Types를 사용한 동적 Props 정의
Mapped Types를 사용하면 기존 타입을 기반으로 새로운 타입을 동적으로 생성할 수 있습니다:
interface FormFields {
username: string;
email: string;
password: string;
}
type FormProps = {
[K in keyof FormFields]: {
value: FormFields[K];
onChange: (value: FormFields[K]) => void;
}
}
const Form: React.FC<FormProps> = ({ username, email, password }) => {
return (
<form>
<input
value={username.value}
onChange={(e) => username.onChange(e.target.value)}
/>
<input
value={email.value}
onChange={(e) => email.onChange(e.target.value)}
/>
<input
type="password"
value={password.value}
onChange={(e) => password.onChange(e.target.value)}
/>
</form>
);
};
이 예제에서는 FormFields
인터페이스의 각 필드에 대해 value와 onChange 프로퍼티를 가진 객체를 생성합니다.
3.4 Conditional Types를 사용한 조건부 Props 정의
Conditional Types를 사용하면 특정 조건에 따라 다른 타입을 사용할 수 있습니다:
type StringOrNumber<T extends boolean> = T extends true ? string : number;
interface ConditionalProps<T extends boolean> {
isString: T;
value: StringOrNumber<T>;
}
const ConditionalComponent = <T extends boolean>({ isString, value }: ConditionalProps<T>) => {
return <div>Value: {value}</div>;
};
// 사용 예:
<ConditionalComponent isString={true} value="Hello" />
<ConditionalComponent isString={false} value={42} />
이 예제에서는 isString
prop의 값에 따라 value
prop의 타입이 결정됩니다.
이러한 고급 타입 정의 기법들을 사용하면 더 유연하고 재사용 가능한 컴포넌트를 만들 수 있습니다. 다음 섹션에서는 이러한 기법들을 실제 프로젝트에 적용할 때의 모범 사례와 주의사항에 대해 알아보겠습니다.
4. Props 타입 정의의 모범 사례와 패턴 🌟
지금까지 다양한 props 타입 정의 방법을 살펴보았습니다. 이제 실제 프로젝트에서 이러한 기법들을 효과적으로 사용하기 위한 모범 사례와 패턴에 대해 알아보겠습니다.
4.1 Props 인터페이스 명명 규칙
일관성 있는 명명 규칙을 사용하면 코드의 가독성과 유지보수성이 향상됩니다. 일반적으로 다음과 같은 규칙을 따릅니다:
- 컴포넌트 이름 + Props: 예)
ButtonProps
,UserProfileProps
- HOC(Higher-Order Component)의 경우: With + 기능 + Props: 예)
WithLoadingProps
,WithAuthProps
interface ButtonProps {
// ...
}
interface UserProfileProps {
// ...
}
interface WithLoadingProps {
// ...
}
4.2 Props 그룹화 및 구조화
관련된 props를 그룹화하면 복잡한 컴포넌트의 props를 더 쉽게 관리할 수 있습니다:
interface AddressProps {
street: string;
city: string;
country: string;
}
interface UserProps {
name: string;
age: number;
address: AddressProps;
}
const UserProfile: React.FC<UserProps> = ({ name, age, address }) => {
// ...
};
4.3 재사용 가능한 타입 정의
여러 컴포넌트에서 공통으로 사용되는 props 타입은 별도의 파일로 분리하여 재사용할 수 있습니다:
// types.ts
export interface SizeProps {
size: 'small' | 'medium' | 'large';
}
export interface ColorProps {
color: 'primary' | 'secondary' | 'danger';
}
// Button.tsx
import { SizeProps, ColorProps } from './types';
interface ButtonProps extends SizeProps, ColorProps {
label: string;
onClick: () => void;
}
const Button: React.FC<ButtonProps> = ({ size, color, label, onClick }) => {
// ...
};
4.4 Prop Drilling 방지
Prop Drilling은 여러 단계의 컴포넌트를 거쳐 props를 전달하는 상황을 말합니다. 이는 코드를 복잡하게 만들고 유지보수를 어렵게 할 수 있습니다. 이를 방지하기 위해 Context API나 상태 관리 라이브러리를 사용할 수 있습니다:
import React, { createContext, useContext } from 'react';
interface ThemeContextProps {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = React.useState<'light' | 'dark'>('light');
const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
4.5 Props의 기본값 설정
선택적 props에 대해 기본값을 설정하면 컴포넌트의 사용성을 높일 수 있습니다:
interface GreetingProps {
name: string;
greeting?: string;
}
const Greeting: React.FC<GreetingProps> = ({ name, greeting = 'Hello' }) => {
return <h1>{greeting}, {name}!</h1>;
};
4.6 Props 타입 검증
런타임에서 props의 타입을 검증하려면 prop-types 라이브러리를 사용할 수 있습니다. TypeScript와 함께 사용하면 이중 안전장치가 됩니다:
import PropTypes from 'prop-types';
interface UserProps {
name: string;
age: number;
}
const User: React.FC<UserProps> = ({ name, age }) => {
return <div>{name} ({age})</div>;
};
User.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
};
이러한 모범 사례와 패턴을 따르면 React와 TypeScript를 사용한 프로젝트의 코드 품질과 유지보수성을 크게 향상시킬 수 있습니다. 다음 섹션에서는 실제 프로젝트에서 자주 마주치는 문제들과 그 해결 방법에 대해 알아보겠습니다.
5. 실제 프로젝트에서의 문제와 해결책 🛠️
실제 프로젝트에서 React와 TypeScript를 사용하다 보면 다양한 문제에 직면할 수 있습니다. 이 섹션에서는 자주 발생하는 문제들과 그 해결 방법에 대해 알아보겠습니다.
5.1 any 타입 사용 최소화하기
any
타입은 TypeScript의 타입 체크를 무력화시키므로 가능한 한 사용을 피해야 합니다. 대신 다음과 같은 방법을 사용할 수 있습니다:
// 나쁜 예
const handleChange = (event: any) => {
// ...
};
// 좋은 예
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// ...
};
5.2 Union 타입과 타입 가드 활용하기
여러 가지 타입을 다루어야 할 때는 Union 타입과 타입 가드를 활용할 수 있습니다:
type ButtonVariant = 'primary' | 'secondary' | 'danger';
interface ButtonProps {
variant: ButtonVariant;
label: string;
}
const Button: React.FC<ButtonProps> = ({ variant, label }) => {
const getButtonClass = (v: ButtonVariant): string => {
switch (v) {
case 'primary': return 'btn-primary';
case 'secondary': return 'btn-secondary';
case 'danger': return 'btn-danger';
default:
const _exhaustiveCheck: never = v;
return _exhaustiveCheck;
}
};
return <button className={getButtonClass(variant)}>{label}</button>;
};
5.3 제네릭 컴포넌트 만들기
재사용 가능한 컴포넌트를 만들 때는 제네릭을 활용할 수 있습니다:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
);
}
// 사용 예
<List
items={[1, 2, 3, 4, 5]}
renderItem={(item) => <span>Number {item}</span>}
/>
5.4 비동기 데이터 처리하기
API 호출 등 비동기 작업의 결과를 타입 안전하게 처리하려면 다음과 같은 방법을 사용할 수 있습니다:
interface User {
id: number;
name: string;
}
interface UserState {
data: User | null;
loading: boolean;
error: string | null;
}
const [userState, setUserState] = useState<UserState>({
data: null,
loading: false,
error: null
});
const fetchUser = async (id: number) => {
setUserState({ ...userState, loading: true });
try {
const response = await fetch(`https://api.example.com/users/${id}`);
const data: User = await response.json();
setUserState({ data, loading: false, error: null });
} catch (error) {
setUserState({ data: null, loading: false, error: 'Failed to fetch user' });
}
};
5.5 타입 단언 사용하기
때로는 TypeScript보다 개발자가 더 정확한 타입 정보를 알고 있을 때가 있습니다. 이런 경우 타입 단언을 사용할 수 있지만, 주의해서 사용해야 합니다:
const myCanvas = document.getElementById('main_canvas') as HTMLCanvasElement;
5.6 타입 정의 파일 (.d.ts) 활용하기
서드파티 라이브러리나 전역 변수에 대한 타입 정의가 필요할 때는 타입 정의 파일을 사용할 수 있습니다:
// global.d.ts
declare global {
interface Window {
myCustomGlobal: string;
}
}
// 사용
window.myCustomGlobal = 'Hello, TypeScript!';
이러한 문제 해결 방법들을 숙지하고 적절히 활용하면, React와 TypeScript를 사용한 프로젝트에서 발생할 수 있는 대부분의 타입 관련 문제를 효과적으로 해결할 수 있습니다. 다음 섹션에서는 이 모든 내용을 종합하여 실제 프로젝트에 적용하는 방법에 대해 알아보겠습니다.
6. 실전 프로젝트: 쇼핑몰 컴포넌트 구현하기 🛍️
지금까지 배운 내용을 종합하여 실제 프로젝트에서 사용할 수 있는 쇼핑몰 컴포넌트를 구현해보겠습니다. 이 예제를 통해 React와 TypeScript를 함께 사용하는 방법을 실전적으로 이해할 수 있을 것입니다.
6.1 타입 정의
먼저 필요한 타입들을 정의합니다:
// types.ts
export interface Product {
id: number;
name: string;
price: number;
description: string;
imageUrl: string;
}
export interface CartItem extends Product {
quantity: number;
}
export type AddToCartFunction = (product: Product) => void;
export type RemoveFromCartFunction = (productId: number) => void;
6.2 제품 목록 컴포넌트
제품 목록을 표시하는 컴포넌트를 만듭니다:
// ProductList.tsx
import React from 'react';
import { Product, AddToCartFunction } from './types';
interface ProductListProps {
products: Product[];
addToCart: AddToCartFunction;
}
export const ProductList: React.FC<ProductListProps> = ({ products, addToCart }) => {
return (
<div className="product-list">
{products.map(product => (
<div key={product.id} className="product-item">
<img src={product.imageUrl} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price.toFixed(2)}</p>
<button onClick={() => addToCart(product)}>Add to Cart</button>
</div>
))}
</div>
);
};
6.3 장바구니 컴포넌트
장바구니 내용을 표시하는 컴포넌트를 만듭니다:
// Cart.tsx
import React from 'react';
import { CartItem, RemoveFromCartFunction } from './types';
interface CartProps {
items: CartItem[];
removeFromCart: RemoveFromCartFunction;
}
export const Cart: React.FC<CartProps> = ({ items, removeFromCart }) => {
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
return (
<div className="cart">
<h2>Your Cart</h2>
{items.map(item => (
<div key={item.id} className="cart-item">
<span>{item.name} x {item.quantity}</span>
<span>${(item.price * item.quantity).toFixed(2)}</span>
<button onClick={() => removeFromCart(item.id)}>Remove</button>
</div>
))}
<div className="cart-total">
<strong>Total: ${total.toFixed(2)}</strong>
</div>
</div>
);
};
6.4 메인 App 컴포넌트
이제 이 모든 것을 조합하는 메인 App 컴포넌트를 만듭니다:
// App.tsx
import React, { useState } from 'react';
import { ProductList } from './ProductList';
import { Cart } from './Cart';
import { Product, CartItem } from './types';
const initialProducts: Product[] = [
{ id: 1, name: "Laptop", price: 999.99, description: "Powerful laptop", imageUrl: "laptop.jpg" },
{ id: 2, name: "Smartphone", price: 499.99, description: "Latest smartphone", imageUrl: "phone.jpg" },
// 더 많은 제품들...
];
export const App: React.FC = () => {
const [cartItems, setCartItems] = useState<CartItem[]>([]);
const addToCart: AddToCartFunction = (product) => {
setCartItems(prevItems => {
const existingItem = prevItems.find(item => item.id === product.id);
if (existingItem) {
return prevItems.map(item =>
item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
);
}
return [...prevItems, { ...product, quantity: 1 }];
});
};
const removeFromCart: RemoveFromCartFunction = (productId) => {
setCartItems(prevItems => prevItems.filter(item => item.id !== productId));
};
return (
<div className="app">
<h1>My Online Store</h1>
<ProductList products={initialProducts} addToCart={addToCart} />
<Cart items={cartItems} removeFromCart={removeFromCart} />
</div>
);
};
이 예제에서는 다음과 같은 TypeScript와 React의 기능들을 활용했습니다:
- 인터페이스를 사용한 props 타입 정의
- 제네릭을 활용한 useState 훅 사용
- 함수 타입 정의 및 활용
- 배열 메서드와 함께 사용되는 타입 추론
이러한 방식으로 TypeScript를 활용하면, 코드의 안정성을 높이고 개발 과정에서 발생할 수 있는 많은 오류를 사전에 방지할 수 있습니다.
이 실전 프로젝트 예제를 통해 React와 TypeScript를 함께 사용하는 방법을 종합적으로 이해할 수 있습니다. 이러한 패턴과 기법들을 자신의 프로젝트에 적용하면, 더욱 안정적이고 유지보수가 용이한 애플리케이션을 개발할 수 있을 것입니다.
7. 결론 및 추가 학습 자료 📚
이 글에서 우리는 React와 TypeScript를 함께 사용하여 컴포넌트의 props 타입을 정의하는 방법에 대해 깊이 있게 살펴보았습니다. 기본적인 타입 정의부터 시작하여 고급 기법, 실제 프로젝트에서의 적용 방법까지 다양한 내용을 다루었습니다.
React와 TypeScript의 조합은 강력한 타입 체크 기능을 통해 개발 과정에서의 오류를 줄이고, 코드의 가독성과 유지보수성을 크게 향상시킵니다. 이는 특히 대규모 프로젝트나 팀 단위의 개발에서 큰 이점을 제공합니다.
하지만 여기서 배운 내용은 시작에 불과합니다. React와 TypeScript의 세계는 매우 넓고 깊기 때문에, 지속적인 학습과 실습이 필요합니다. 다음은 추가 학습을 위한 몇 가지 자료들입니다:
- 공식 TypeScript 문서: https://www.typescriptlang.org/docs/
- React TypeScript Cheatsheet: https://github.com/typescript-cheatsheets/react
- TypeScript Deep Dive: https://basarat.gitbook.io/typescript/
- React + TypeScript 예제 프로젝트: https://github.com/piotrwitek/react-redux-typescript-guide
React와 TypeScript를 함께 사용하는 것은 처음에는 약간의 학습 곡선이 있을 수 있지만, 익숙해지면 개발 생산성과 코드 품질을 크게 향상시킬 수 있습니다. 지속적인 학습과 실습을 통해 이 강력한 도구들을 마스터하시기 바랍니다.
마지막으로, 기술의 세계는 빠르게 변화하고 있습니다. React와 TypeScript도 계속해서 발전하고 있으므로, 최신 트렌드와 업데이트를 주시하는 것이 중요합니다. 개발자 커뮤니티에 참여하고, 오픈 소스 프로젝트에 기여하는 것도 좋은 학습 방법이 될 수 있습니다.
여러분의 React와 TypeScript 여정에 행운이 있기를 바랍니다! 해피 코딩! 🚀