Skip to content

Latest commit

 

History

History
813 lines (665 loc) · 15.9 KB

File metadata and controls

813 lines (665 loc) · 15.9 KB

01. React 기초

1. React란?

React는 사용자 인터페이스(UI)를 만들기 위한 JavaScript 라이브러리입니다. 컴포넌트 기반으로 재사용 가능한 UI를 만들 수 있으며, 상태 변화에 따라 효율적으로 화면을 업데이트합니다.

React의 특징

  • 컴포넌트 기반: UI를 독립적이고 재사용 가능한 조각으로 나눔
  • 선언적: "어떻게"가 아닌 "무엇을" 보여줄지 작성
  • Virtual DOM: 변경사항을 효율적으로 실제 DOM에 반영

2. 개발 환경 설정

Vite로 React + TypeScript 프로젝트 생성

# 새 프로젝트 생성
npm create vite@latest my-react-app -- --template react-ts

# 프로젝트 디렉토리로 이동
cd my-react-app

# 패키지 설치
npm install

# 개발 서버 실행
npm run dev

프로젝트 구조

my-react-app/
├── public/              # 정적 파일
├── src/
│   ├── App.tsx         # 메인 컴포넌트
│   ├── main.tsx        # 진입점
│   ├── App.css         # 스타일
│   └── index.css       # 전역 스타일
├── index.html          # HTML 템플릿
├── package.json        # 프로젝트 설정
└── tsconfig.json       # TypeScript 설정

3. 컴포넌트와 JSX

JSX (JavaScript XML)

JavaScript 안에서 HTML과 비슷한 문법으로 UI를 작성할 수 있습니다.

// App.tsx
function App() {
  return (
    <div className="container">
      <h1>안녕하세요, React!</h1>
      <p>JSX를 사용하여 UI를 작성합니다.</p>
    </div>
  );
}

export default App;

JSX 규칙

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>
    </>
  );
}

4. 함수형 컴포넌트 ⭐

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;

TypeScript와 함께 사용

// 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>
  );
}

5. Props - 데이터 전달 ⭐⭐

Props는 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 방법입니다.

기본 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>
  );
}

선택적 Props와 기본값

interface GreetingProps {
  name: string;
  greeting?: string;  // 선택적 prop
}

function Greeting({ name, greeting = "안녕하세요" }: GreetingProps) {
  return <h1>{greeting}, {name}님!</h1>;
}

// 사용
<Greeting name="김철수" />                    // "안녕하세요, 김철수님!"
<Greeting name="박영희" greeting="Hello" />  // "Hello, 박영희님!"

children Props

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>
  );
}

Props 전달 패턴

// 객체로 사용자 정보 전달
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} />;
}

6. 컴포넌트 분리와 재사용

작은 컴포넌트로 나누기

// 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>
  );
}

7. 조건부 렌더링

if-else (삼항 연산자)

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} />                    // "로그인해주세요."

논리 AND (&&) 연산자

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>
  );
}

8. 리스트 렌더링 ⭐⭐

배열 데이터를 화면에 표시할 때 map() 메서드를 사용합니다.

기본 리스트

function FruitList() {
  const fruits = ["사과", "바나나", "오렌지"];

  return (
    <ul>
      {fruits.map((fruit, index) => (
        <li key={index}>{fruit}</li>
      ))}
    </ul>
  );
}

key의 중요성 ⭐

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>
  );
}

9. 이벤트 처리

기본 이벤트 핸들러

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>
  );
}

10. 실전 예제: 간단한 카운터

// 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;

실습 과제

다음 컴포넌트들을 직접 만들어보세요:

1. ProfileCard 컴포넌트

// TODO: 사용자 프로필 카드 컴포넌트 작성
// Props: name, age, email, avatarUrl, bio (선택적)
// avatarUrl이 없으면 기본 아바타 표시

2. ProductList 컴포넌트

// TODO: 상품 목록 컴포넌트 작성
// Props: products 배열
// 각 상품은 id, name, price, category를 가짐
// 카테고리별로 다른 색상의 배지 표시

3. SearchBar 컴포넌트

// TODO: 검색창 컴포넌트 작성
// Props: placeholder, onSearch (검색 버튼 클릭 시 호출)
// 입력값을 부모 컴포넌트로 전달

4. TodoList 미리보기

// TODO: 간단한 Todo 목록 컴포넌트
// 할 일 목록을 표시 (체크박스, 텍스트)
// 완료된 항목은 취소선 표시
// State는 아직 사용하지 말고 props로만 구현

다음 섹션에서는 State와 Hooks를 배워 실제로 데이터를 변경하고 상호작용하는 컴포넌트를 만들어봅니다!