Ch.12 React 상태 관리 — 살아 움직이는 UI

미니 프로젝트 — 투두 앱

CRUD(생성, 읽기, 수정, 삭제) 기능을 React로 구현할 수 있다기능별로 컴포넌트를 분리하고 props로 데이터를 전달할 수 있다state를 적절한 위치에 배치하여 데이터 흐름을 설계할 수 있다

배운 것을 모아 진짜 앱을 만든다면?

useState, 이벤트, 리스트 렌더링, 조건부 표시 — Ch11~12에서 배운 모든 것을 투두 앱 하나로 통합합니다.

여러 기능을 하나의 앱으로 조합하려면?

컴포넌트 설계 — 기능별로 컴포넌트를 나누고, state를 적절히 배치하는 것이 React의 핵심입니다.


article

핵심 내용

먼저 전체 구조를 설계하고 컴포넌트를 나눠봅시다

입력: 할 일 텍스트 입력 + 추가 버튼

추가: 새 할 일이 목록에 추가됨

목록: 할 일 리스트 표시 + 완료 체크

삭제/완료: 완료 토글, 삭제 기능

state는 어디에 둘까? todos 배열은 TodoApp (최상위)에 두어야 합니다. 이유: TodoForm(추가)과 TodoList(표시/삭제) 모두 todos를 사용하므로 → 공통 부모에 state를 올려놓고, props로 내려보냅니다. 이것을 '상태 끌어올리기(Lifting State Up)' 라고 합니다.

전체 앱의 구조와 state 설계를 살펴봅시다

import { useState } from "react";

function TodoApp() {
  // state: 할 일 목록 배열
  const [todos, setTodos] = useState([
    { id: 1, text: "React 공부하기", completed: false },
    { id: 2, text: "투두 앱 만들기", completed: false },
  ]);

  // 다음 ID를 위한 카운터
  const [nextId, setNextId] = useState(3);

  // 필터 상태 (all / active / completed)
  const [filter, setFilter] = useState("all");

  return (
    <div className="todo-app">
      <h1>📝 할 일 목록</h1>

      {/* 입력 폼 */}
      <TodoForm onAdd={addTodo} />

      {/* 필터 버튼 */}
      <div className="filters">
        <button onClick={() => setFilter("all")}>전체</button>
        <button onClick={() => setFilter("active")}>진행 중</button>
        <button onClick={() => setFilter("completed")}>완료</button>
      </div>

      {/* 할 일 목록 */}
      <TodoList
        todos={filteredTodos}
        onToggle={toggleTodo}
        onDelete={deleteTodo}
      />

      {/* 통계 */}
      <p>{todos.filter(t => !t.completed).length}개 남음</p>
    </div>
  );
}

Todo 데이터 구조 각 할 일은 객체로 관리합니다: ``` { id: 1, text: "React 공부하기", completed: false } ``` • id — 고유 식별자 (key로 사용) • text — 할 일 내용 • completed — 완료 여부 (boolean)

배열을 변경할 때는 새 배열을 만들어야 합니다 (불변성)

// ── 추가 (Create) ──
const addTodo = (text) => {
  const newTodo = {
    id: nextId,
    text: text,
    completed: false,
  };

  // 스프레드로 새 배열 생성 (기존 배열 수정 X)
  setTodos([...todos, newTodo]);
  setNextId(nextId + 1);
};

// ── 삭제 (Delete) ──
const deleteTodo = (id) => {
  // filter로 해당 id를 제외한 새 배열 생성
  setTodos(todos.filter(todo => todo.id !== id));
};

// ── TodoForm 컴포넌트 ──
function TodoForm({ onAdd }) {
  const [text, setText] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim() === "") return; // 빈 값 방지

    onAdd(text);   // 부모의 addTodo 호출
    setText("");    // 입력 필드 초기화
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="할 일을 입력하세요"
      />
      <button type="submit">추가</button>
    </form>
  );
}

map()으로 특정 항목만 변경한 새 배열을 만듭니다

// ── 완료 토글 (Update) ──
const toggleTodo = (id) => {
  setTodos(
    todos.map(todo =>
      todo.id === id
        ? { ...todo, completed: !todo.completed }  // 해당 항목만 변경
        : todo  // 나머지는 그대로
    )
  );
};

// ── 필터링 (파생 데이터) ──
const filteredTodos = todos.filter(todo => {
  if (filter === "active") return !todo.completed;
  if (filter === "completed") return todo.completed;
  return true;  // "all"
});

// ── TodoItem 컴포넌트 ──
function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li className={todo.completed ? "completed" : ""}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span style={{
        textDecoration: todo.completed ? "line-through" : "none"
      }}>
        {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>삭제</button>
    </li>
  );
}

불변성 원칙 정리 React에서 state를 업데이트할 때 반드시 새 값/새 배열/새 객체를 만들어야 합니다. • 추가: `[...기존배열, 새항목]` (스프레드 연산자) • 삭제: `배열.filter(조건)` (조건에 맞는 것만 남기기) • 수정: `배열.map(항목 => 조건 ? 변경 : 그대로)` (해당 항목만 변경) 이것이 React의 불변성(Immutability) 원칙입니다!

React에서 배열 state에 새 항목을 추가할 때 올바른 방법은?

todos.push(newTodo)를 사용하면 React가 자동으로 리렌더링한다

여러 자식 컴포넌트가 공유하는 state는 ___ 에 두어야 한다

React 기초 완전 정복!

key

핵심 용어

🏠

TodoApp

최상위 컴포넌트 — todos state를 관리, 자식에게 props 전달

📝

TodoForm

입력 폼 — 새 할 일 텍스트 입력과 추가 버튼

📋

TodoList

목록 영역 — TodoItem 컴포넌트를 map()으로 렌더링

☑️

TodoItem

개별 할 일 — 완료 토글, 삭제 버튼 포함

edit_note

정리 노트

미니 프로젝트 — 투두 앱 정리

CRUD 패턴

Create
setState([...prev, newItem])로 새 항목 추가
Read
state 배열을 map()으로 렌더링
Update
map()으로 해당 id 찾아서 속성 변경
Delete
filter()로 해당 id 제외한 새 배열 생성

설계 원칙

상태 위치
공통 부모(App)에 상태를 두고 자식에 props 전달
컴포넌트 분리
TodoInput, TodoList, TodoItem으로 역할별 분리
불변 업데이트
배열/객체를 직접 수정하지 않고 새로 생성

CRUD는 거의 모든 앱의 기본 패턴입니다 — 투두 앱으로 완벽히 익혀두세요.

check_circle

핵심 정리

  • 1Ch11-1: React = 컴포넌트 기반 UI 라이브러리, SPA로 빠른 사용자 경험
  • 2Ch11-2: JSX = JS 안의 HTML, Props = 부모→자식 데이터 전달
  • 3Ch11-3: map()으로 리스트 렌더링, key로 요소 식별, 조건부 렌더링 3패턴
  • 4Ch12-1: useState = 상태 관리 Hook, setter 호출 → 리렌더링
  • 5Ch12-2: 이벤트 핸들링 + Controlled Component + 폼 처리
  • 6Ch12-3: useEffect = 부수 효과 처리, 의존성 배열로 실행 조건 제어
  • 7Ch12-4: 투두 앱 — CRUD, 컴포넌트 분리, 불변성 원칙, 상태 끌어올리기
  • 8이제 React로 실제 웹 애플리케이션을 만들 수 있습니다!

퀴즈와 인터랙션으로 더 깊이 학습하세요

play_circle인터랙티브 레슨 시작