React는 사용자 인터페이스(UI)를 만들기 위한 JavaScript 라이브러리입니다. 컴포넌트 기반으로 재사용 가능한 UI를 만들 수 있으며, 상태 변화에 따라 효율적으로 화면을 업데이트합니다.
- 컴포넌트 기반: UI를 독립적이고 재사용 가능한 조각으로 나눔
- 선언적: "어떻게"가 아닌 "무엇을" 보여줄지 작성
- Virtual DOM: 변경사항을 효율적으로 실제 DOM에 반영
# 새 프로젝트 생성
npm create vite@latest my-react-app -- --template react-ts
# 프로젝트 디렉토리로 이동
cd my-react-app
# 패키지 설치
npm install
# 개발 서버 실행
npm run devmy-react-app/
├── public/ # 정적 파일
├── src/
│ ├── App.tsx # 메인 컴포넌트
│ ├── main.tsx # 진입점
│ ├── App.css # 스타일
│ └── index.css # 전역 스타일
├── index.html # HTML 템플릿
├── package.json # 프로젝트 설정
└── tsconfig.json # TypeScript 설정
JavaScript 안에서 HTML과 비슷한 문법으로 UI를 작성할 수 있습니다.
// App.tsx
function App() {
return (
<div className="container">
<h1>안녕하세요, React!</h1>
<p>JSX를 사용하여 UI를 작성합니다.</p>
</div>
);
}
export default App;function Example() {
const name = "김철수";
const age = 25;
return (
<div>
{/* 1. 중괄호 {}로 JavaScript 표현식 삽입 */}
<h1>안녕하세요, {name}님!</h1>
<p>나이: {age}살</p>
<p>내년 나이: {age + 1}살</p>
{/* 2. className 사용 (class는 예약어) */}
<div className="box">박스</div>
{/* 3. 인라인 스타일은 객체로 */}
<div style={{ color: "red", fontSize: "20px" }}>
빨간 텍스트
</div>
{/* 4. 모든 태그는 닫아야 함 */}
<input type="text" />
<img src="image.jpg" alt="이미지" />
{/* 5. 주석은 {/* */} 형식 */}
</div>
);
}중요 규칙:
- 컴포넌트는 반드시 하나의 최상위 요소를 반환해야 함
class→className,for→htmlFor- 카멜케이스 사용:
onclick→onClick,tabindex→tabIndex
// ❌ 잘못된 예 - 여러 최상위 요소
function Wrong() {
return (
<h1>제목</h1>
<p>내용</p>
);
}
// ✅ 올바른 예 1 - div로 감싸기
function Correct1() {
return (
<div>
<h1>제목</h1>
<p>내용</p>
</div>
);
}
// ✅ 올바른 예 2 - Fragment 사용 (권장)
function Correct2() {
return (
<>
<h1>제목</h1>
<p>내용</p>
</>
);
}React 컴포넌트는 UI를 반환하는 함수입니다.
// Greeting.tsx
function Greeting() {
return <h1>안녕하세요!</h1>;
}
export default Greeting;// App.tsx
import Greeting from "./Greeting";
function App() {
return (
<div>
<Greeting />
</div>
);
}
export default App;// Button.tsx
interface ButtonProps {
text: string;
color?: string;
}
function Button({ text, color = "blue" }: ButtonProps) {
return (
<button style={{ backgroundColor: color }}>
{text}
</button>
);
}
export default Button;// App.tsx
import Button from "./Button";
function App() {
return (
<div>
<Button text="클릭하세요" />
<Button text="제출" color="green" />
</div>
);
}Props는 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 방법입니다.
// UserCard.tsx
interface UserCardProps {
name: string;
age: number;
email: string;
}
function UserCard({ name, age, email }: UserCardProps) {
return (
<div className="user-card">
<h2>{name}</h2>
<p>나이: {age}살</p>
<p>이메일: {email}</p>
</div>
);
}
export default UserCard;// App.tsx
import UserCard from "./UserCard";
function App() {
return (
<div>
<UserCard name="김철수" age={25} email="kim@example.com" />
<UserCard name="박영희" age={30} email="park@example.com" />
</div>
);
}interface GreetingProps {
name: string;
greeting?: string; // 선택적 prop
}
function Greeting({ name, greeting = "안녕하세요" }: GreetingProps) {
return <h1>{greeting}, {name}님!</h1>;
}
// 사용
<Greeting name="김철수" /> // "안녕하세요, 김철수님!"
<Greeting name="박영희" greeting="Hello" /> // "Hello, 박영희님!"interface CardProps {
children: React.ReactNode;
title: string;
}
function Card({ children, title }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-content">
{children}
</div>
</div>
);
}
// 사용
function App() {
return (
<Card title="사용자 정보">
<p>이름: 김철수</p>
<p>나이: 25살</p>
<button>프로필 보기</button>
</Card>
);
}// 객체로 사용자 정보 전달
interface User {
id: number;
name: string;
email: string;
age: number;
}
interface UserProfileProps {
user: User;
}
function UserProfile({ user }: UserProfileProps) {
return (
<div>
<h2>{user.name}</h2>
<p>이메일: {user.email}</p>
<p>나이: {user.age}살</p>
</div>
);
}
// 사용
function App() {
const user: User = {
id: 1,
name: "김철수",
email: "kim@example.com",
age: 25
};
return <UserProfile user={user} />;
}// ProductCard.tsx
interface Product {
id: number;
name: string;
price: number;
image: string;
}
interface ProductCardProps {
product: Product;
}
function ProductCard({ product }: ProductCardProps) {
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price.toLocaleString()}원</p>
<button>장바구니에 담기</button>
</div>
);
}
export default ProductCard;// ProductList.tsx
import ProductCard from "./ProductCard";
interface ProductListProps {
products: Product[];
}
function ProductList({ products }: ProductListProps) {
return (
<div className="product-list">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
export default ProductList;// App.tsx
import ProductList from "./ProductList";
function App() {
const products: Product[] = [
{ id: 1, name: "노트북", price: 1500000, image: "laptop.jpg" },
{ id: 2, name: "마우스", price: 30000, image: "mouse.jpg" },
{ id: 3, name: "키보드", price: 80000, image: "keyboard.jpg" }
];
return (
<div>
<h1>상품 목록</h1>
<ProductList products={products} />
</div>
);
}interface WelcomeProps {
isLoggedIn: boolean;
userName?: string;
}
function Welcome({ isLoggedIn, userName }: WelcomeProps) {
return (
<div>
{isLoggedIn ? (
<h1>환영합니다, {userName}님!</h1>
) : (
<h1>로그인해주세요.</h1>
)}
</div>
);
}
// 사용
<Welcome isLoggedIn={true} userName="김철수" /> // "환영합니다, 김철수님!"
<Welcome isLoggedIn={false} /> // "로그인해주세요."interface NotificationProps {
hasNewMessages: boolean;
messageCount: number;
}
function Notification({ hasNewMessages, messageCount }: NotificationProps) {
return (
<div>
<h2>알림</h2>
{hasNewMessages && (
<p>새 메시지 {messageCount}개가 있습니다.</p>
)}
</div>
);
}
// 사용
<Notification hasNewMessages={true} messageCount={5} /> // 메시지 표시
<Notification hasNewMessages={false} messageCount={0} /> // 메시지 숨김interface StatusBadgeProps {
status: "pending" | "success" | "error";
}
function StatusBadge({ status }: StatusBadgeProps) {
let color = "";
let text = "";
if (status === "pending") {
color = "yellow";
text = "대기 중";
} else if (status === "success") {
color = "green";
text = "완료";
} else {
color = "red";
text = "오류";
}
return (
<span style={{ backgroundColor: color, padding: "5px 10px" }}>
{text}
</span>
);
}
// 또는 객체 매핑 사용 (더 깔끔함)
function StatusBadge2({ status }: StatusBadgeProps) {
const statusConfig = {
pending: { color: "yellow", text: "대기 중" },
success: { color: "green", text: "완료" },
error: { color: "red", text: "오류" }
};
const config = statusConfig[status];
return (
<span style={{ backgroundColor: config.color, padding: "5px 10px" }}>
{config.text}
</span>
);
}배열 데이터를 화면에 표시할 때 map() 메서드를 사용합니다.
function FruitList() {
const fruits = ["사과", "바나나", "오렌지"];
return (
<ul>
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
</ul>
);
}interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoListProps {
todos: Todo[];
}
function TodoList({ todos }: TodoListProps) {
return (
<ul>
{todos.map(todo => (
// ✅ 고유한 id를 key로 사용 (권장)
<li key={todo.id}>
{todo.text}
</li>
))}
</ul>
);
}
// ❌ 인덱스를 key로 사용하는 것은 권장되지 않음 (항목 순서가 바뀔 수 있을 때)
// ✅ 고유한 id가 없고 순서가 절대 바뀌지 않을 때만 인덱스 사용key를 사용하는 이유:
- React가 어떤 항목이 변경/추가/삭제되었는지 식별
- 성능 최적화
- 컴포넌트 상태 유지
interface User {
id: number;
name: string;
email: string;
role: string;
}
interface UserTableProps {
users: User[];
}
function UserTable({ users }: UserTableProps) {
return (
<table>
<thead>
<tr>
<th>이름</th>
<th>이메일</th>
<th>역할</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.role}</td>
</tr>
))}
</tbody>
</table>
);
}interface Product {
id: number;
name: string;
price: number;
category: string;
}
interface ProductListProps {
products: Product[];
category?: string;
}
function ProductList({ products, category }: ProductListProps) {
// 카테고리 필터링
const filteredProducts = category
? products.filter(p => p.category === category)
: products;
return (
<div>
<h2>상품 목록</h2>
{filteredProducts.length === 0 ? (
<p>상품이 없습니다.</p>
) : (
<ul>
{filteredProducts.map(product => (
<li key={product.id}>
{product.name} - {product.price.toLocaleString()}원
</li>
))}
</ul>
)}
</div>
);
}function ButtonExample() {
const handleClick = () => {
console.log("버튼이 클릭되었습니다!");
};
return (
<div>
{/* 방법 1: 함수 참조 전달 */}
<button onClick={handleClick}>클릭</button>
{/* 방법 2: 인라인 화살표 함수 */}
<button onClick={() => console.log("인라인 클릭")}>
인라인 클릭
</button>
</div>
);
}function InputExample() {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log("입력값:", e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // 기본 동작 방지
console.log("폼 제출");
};
return (
<form onSubmit={handleSubmit}>
<input type="text" onChange={handleChange} />
<button type="submit">제출</button>
</form>
);
}interface ButtonListProps {
items: string[];
}
function ButtonList({ items }: ButtonListProps) {
const handleClick = (item: string) => {
console.log(`${item}이(가) 클릭되었습니다.`);
};
return (
<div>
{items.map((item, index) => (
<button key={index} onClick={() => handleClick(item)}>
{item}
</button>
))}
</div>
);
}
// 사용
<ButtonList items={["사과", "바나나", "오렌지"]} />function EventExamples() {
// 클릭 이벤트
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log("클릭 위치:", e.clientX, e.clientY);
};
// 입력 변경 이벤트
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log("입력값:", e.target.value);
};
// 포커스 이벤트
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
console.log("포커스됨");
};
// 키보드 이벤트
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
console.log("엔터 키 입력");
}
};
return (
<div>
<button onClick={handleClick}>클릭</button>
<input
type="text"
onChange={handleChange}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
/>
</div>
);
}// Counter.tsx
import { useState } from "react";
function Counter() {
// State는 다음 섹션에서 자세히 다룹니다
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
const reset = () => {
setCount(0);
};
return (
<div style={{ textAlign: "center", padding: "20px" }}>
<h1>카운터</h1>
<h2 style={{ fontSize: "48px" }}>{count}</h2>
<div>
<button onClick={decrement}>-</button>
<button onClick={reset} style={{ margin: "0 10px" }}>
초기화
</button>
<button onClick={increment}>+</button>
</div>
</div>
);
}
export default Counter;다음 컴포넌트들을 직접 만들어보세요:
// TODO: 사용자 프로필 카드 컴포넌트 작성
// Props: name, age, email, avatarUrl, bio (선택적)
// avatarUrl이 없으면 기본 아바타 표시// TODO: 상품 목록 컴포넌트 작성
// Props: products 배열
// 각 상품은 id, name, price, category를 가짐
// 카테고리별로 다른 색상의 배지 표시// TODO: 검색창 컴포넌트 작성
// Props: placeholder, onSearch (검색 버튼 클릭 시 호출)
// 입력값을 부모 컴포넌트로 전달// TODO: 간단한 Todo 목록 컴포넌트
// 할 일 목록을 표시 (체크박스, 텍스트)
// 완료된 항목은 취소선 표시
// State는 아직 사용하지 말고 props로만 구현다음 섹션에서는 State와 Hooks를 배워 실제로 데이터를 변경하고 상호작용하는 컴포넌트를 만들어봅니다!