6회차 - State 관리

1. State란?

State는 컴포넌트의 데이터 저장소다. 값이 변경되면 화면이 자동으로 다시 그려진다.

기본 사용법

import { useState } from 'react';

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>횟수: {count}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  );
}

2. useState 동작 원리

useState는 두 개의 값을 배열로 반환한다.

const [value, setValue] = useState(초기값);

예제 1: 카운터

function Counter() {
  const [num, setNum] = useState(0);

  const plus = () => setNum(num + 1);
  const minus = () => setNum(num - 1);
  const reset = () => setNum(0);

  return (
    <div>
      <h2>{num}</h2>
      <button onClick={plus}>+</button>
      <button onClick={minus}>-</button>
      <button onClick={reset}>초기화</button>
    </div>
  );
}

3. 여러 개의 State 사용

하나의 컴포넌트에서 여러 state를 만들 수 있다.

function Form() {
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);
  const [email, setEmail] = useState('');

  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="이름"
      />
      <input
        value={age}
        onChange={(e) => setAge(e.target.value)}
        placeholder="나이"
      />
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="이메일"
      />
      <p>이름: {name}, 나이: {age}, 이메일: {email}</p>
    </div>
  );
}

4. 객체로 State 관리

여러 값을 하나의 객체로 묶어 관리할 수 있다.

function UserForm() {
  const [user, setUser] = useState({
    name: '',
    age: 0,
    email: ''
  });

  const change = (e) => {
    setUser({
      ...user,
      [e.target.name]: e.target.value
    });
  };

  return (
    <div>
      <input name="name" value={user.name} onChange={change} />
      <input name="age" value={user.age} onChange={change} />
      <input name="email" value={user.email} onChange={change} />
    </div>
  );
}

5. 배열 State 관리

항목 추가

function List() {
  const [items, setItems] = useState([]);
  const [text, setText] = useState('');

  const add = () => {
    setItems([...items, text]);
    setText('');
  };

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={add}>추가</button>
      <ul>
        {items.map((item, i) => <li key={i}>{item}</li>)}
      </ul>
    </div>
  );
}

항목 삭제

function TodoList() {
  const [todos, setTodos] = useState(['밥먹기', '코딩하기']);

  const remove = (index) => {
    setTodos(todos.filter((_, i) => i !== index));
  };

  return (
    <ul>
      {todos.map((todo, i) => (
        <li key={i}>
          {todo}
          <button onClick={() => remove(i)}>삭제</button>
        </li>
      ))}
    </ul>
  );
}

6. 주의사항

직접 수정 금지

// ❌ 잘못된 방법
count = count + 1;
user.name = '철수';
items.push('새 항목');

// ✅ 올바른 방법
setCount(count + 1);
setUser({...user, name: '철수'});
setItems([...items, '새 항목']);

이전 값 기반 업데이트

연속으로 state를 변경할 때는 함수형 업데이트를 사용한다.

// ❌ 동작 안 함
setCount(count + 1);
setCount(count + 1);
// count는 1만 증가

// ✅ 올바른 방법
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// count가 2 증가

7. 실습: To-Do 앱

import { useState } from 'react';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');

  const add = () => {
    if (input.trim()) {
      setTodos([...todos, { id: Date.now(), text: input, done: false }]);
      setInput('');
    }
  };

  const toggle = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? {...todo, done: !todo.done} : todo
    ));
  };

  const remove = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div>
      <h1>할 일 목록</h1>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && add()}
      />
      <button onClick={add}>추가</button>

      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => toggle(todo.id)}
            />
            <span style={{textDecoration: todo.done ? 'line-through' : 'none'}}>
              {todo.text}
            </span>
            <button onClick={() => remove(todo.id)}>삭제</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoApp;

8. useEffect란?

useEffect는 컴포넌트가 화면에 나타날 때, 사라질 때, 값이 변할 때 특정 작업을 실행한다.

기본 사용법

import { useEffect, useState } from 'react';

function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('화면이 그려짐');
  }, [count]);

  return (
    <div>
      <p>횟수: {count}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  );
}

의존성 배열 (Dependency Array)

의존성 배열은 useEffect가 언제 실행될지 결정한다.

// 1. 의존성 배열 없음 - 매번 렌더링될 때마다 실행
useEffect(() => {
  console.log('매번 실행');
});

// 2. 빈 배열 - 처음 한 번만 실행 (마운트될 때)
useEffect(() => {
  console.log('한 번만 실행');
}, []);

// 3. 값이 들어있음 - 해당 값이 변할 때만 실행
useEffect(() => {
  console.log('count가 변할 때만 실행');
}, [count]);

예제: API 데이터 불러오기

function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    // 페이지 로드할 때 한 번만 실행
    fetch('/api/users')
      .then(res => res.json())
      .then(data => setUsers(data));
  }, []); // 빈 배열: 처음 한 번만

  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

정리 함수 (Cleanup Function)

컴포넌트가 사라질 때 정리 작업을 수행한다.

function Timer() {
  const [sec, setSec] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setSec(prev => prev + 1);
    }, 1000);

    // 컴포넌트가 사라질 때 타이머 멈춤
    return () => clearInterval(timer);
  }, []);

  return <p>경과: {sec}초</p>;
}

9. 연습 과제

  1. 좋아요 버튼 만들기 (클릭하면 숫자 증가)
  2. 입력한 이름을 화면에 실시간 표시
  3. 장바구니 앱 (추가/삭제 기능)
  4. 간단한 계산기 만들기

정리