React 독학 가이드 6 - State로 상태 관리하기

learning by Seven Fingers Studio 18분
ReactStateuseState상태관리Hooks

React에서 진짜 동적인 앱을 만들려면 State가 필수예요. 버튼 클릭하면 숫자가 바뀌고, 입력하면 화면에 보이고… 이런 게 다 State 덕분이에요.

State가 뭔가요?

State는 컴포넌트 내부에서 변할 수 있는 데이터예요.

일반 변수랑 뭐가 다르냐고요? 일반 변수는 바꿔도 화면이 안 바뀌어요.

// ❌ 이렇게 하면 안 됨
function Counter() {
  let count = 0;

  const increase = () => {
    count = count + 1;
    console.log(count);  // 1, 2, 3... 찍히긴 함
  };

  return (
    <div>
      <p>{count}</p>  {/* 계속 0으로 보임! */}
      <button onClick={increase}>+</button>
    </div>
  );
}

변수값이 바뀌어도 React는 “어? 뭐가 바뀌었어?”를 모르거든요. 그래서 화면을 다시 안 그려요.

State를 쓰면:

// ⭕ State 사용
import { useState } from 'react';

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

  const increase = () => {
    setCount(count + 1);  // State 업데이트
  };

  return (
    <div>
      <p>{count}</p>  {/* 1, 2, 3... 화면에 바로 반영! */}
      <button onClick={increase}>+</button>
    </div>
  );
}

실행 결과:

3

버튼 클릭 시 숫자가 증가하며 화면이 업데이트됩니다

State가 바뀌면 React가 컴포넌트를 다시 렌더링해요.

useState 기본 문법

import { useState } from 'react';

const [state, setState] = useState(초기값);
  • state: 현재 상태 값
  • setState: 상태를 변경하는 함수
  • 초기값: 처음 렌더링될 때의 값

다양한 타입의 State

// 숫자
const [count, setCount] = useState(0);

// 문자열
const [name, setName] = useState("");

// 불린
const [isOpen, setIsOpen] = useState(false);

// 배열
const [items, setItems] = useState([]);

// 객체
const [user, setUser] = useState({ name: "", age: 0 });

State 업데이트 방법

1. 직접 값 전달

setCount(10);         // count를 10으로
setName("홍길동");     // name을 "홍길동"으로
setIsOpen(true);      // isOpen을 true로

2. 이전 값 기반으로 업데이트

// ⚠️ 이렇게도 되긴 하지만...
setCount(count + 1);

// ⭕ 이 방법이 더 안전함
setCount(prev => prev + 1);

왜 함수형 업데이트가 더 안전하냐면, React가 State 업데이트를 비동기로 처리하거든요. 빠르게 여러 번 클릭하면 예상과 다르게 동작할 수 있어요.

// ❌ 문제가 생길 수 있음
const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  // count가 1만 증가함!
};// ⭕ 의도대로 3 증가
const handleClick = () => {
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  // count가 3 증가!
};

배열 State 다루기

배열은 직접 수정하면 안 돼요. 새 배열을 만들어야 해요.

항목 추가

const [items, setItems] = useState(["사과", "바나나"]);

// ❌ 직접 수정 (안 됨!)
items.push("오렌지");
setItems(items);

// ⭕ 새 배열 만들기
setItems([...items, "오렌지"]);

항목 삭제

const [todos, setTodos] = useState([
  { id: 1, text: "공부하기" },
  { id: 2, text: "운동하기" },
  { id: 3, text: "청소하기" }
]);

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

항목 수정

const handleUpdate = (id, newText) => {
  setTodos(todos.map(todo =>
    todo.id === id ? { ...todo, text: newText } : todo
  ));
};

객체 State 다루기

객체도 직접 수정하면 안 돼요!

const [user, setUser] = useState({
  name: "홍길동",
  age: 25,
  email: "hong@example.com"
});

// ❌ 직접 수정 (안 됨!)
user.name = "김철수";
setUser(user);

// ⭕ 새 객체 만들기 (spread 사용)
setUser({ ...user, name: "김철수" });

중첩 객체 수정

const [profile, setProfile] = useState({
  name: "홍길동",
  address: {
    city: "서울",
    zipCode: "12345"
  }
});

// 중첩된 객체 수정
setProfile({
  ...profile,
  address: {
    ...profile.address,
    city: "부산"
  }
});

여러 개의 State 사용

State는 여러 개 만들 수 있어요:

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

  return (
    <form>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="이름"
      />
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="이메일"
      />
      <input
        type="number"
        value={age}
        onChange={(e) => setAge(Number(e.target.value))}
        placeholder="나이"
      />
    </form>
  );
}

또는 객체 하나로 관리

function UserForm() {
  const [form, setForm] = useState({
    name: "",
    email: "",
    age: 0
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm({ ...form, [name]: value });
  };

  return (
    <form>
      <input name="name" value={form.name} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
      <input name="age" value={form.age} onChange={handleChange} />
    </form>
  );
}

관련된 데이터면 객체로 묶는 게 편해요.

실전 예제: 장바구니

import { useState } from 'react';

function ShoppingCart() {
  const [cart, setCart] = useState([]);

  const products = [
    { id: 1, name: "에어팟", price: 200000 },
    { id: 2, name: "맥북", price: 2000000 },
    { id: 3, name: "아이패드", price: 1000000 }
  ];

  const addToCart = (product) => {
    const existing = cart.find(item => item.id === product.id);

    if (existing) {
      // 이미 있으면 수량 증가
      setCart(cart.map(item =>
        item.id === product.id
          ? { ...item, quantity: item.quantity + 1 }
          : item
      ));
    } else {
      // 없으면 새로 추가
      setCart([...cart, { ...product, quantity: 1 }]);
    }
  };

  const removeFromCart = (id) => {
    setCart(cart.filter(item => item.id !== id));
  };

  const total = cart.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  return (
    <div>
      <h2>상품 목록</h2>
      {products.map(product => (
        <div key={product.id}>
          <span>{product.name} - {product.price.toLocaleString()}원</span>
          <button onClick={() => addToCart(product)}>담기</button>
        </div>
      ))}

      <h2>장바구니</h2>
      {cart.length === 0 ? (
        <p>비어있습니다</p>
      ) : (
        <>
          {cart.map(item => (
            <div key={item.id}>
              <span>
                {item.name} x {item.quantity} =
                {(item.price * item.quantity).toLocaleString()}원
              </span>
              <button onClick={() => removeFromCart(item.id)}>삭제</button>
            </div>
          ))}
          <h3>총액: {total.toLocaleString()}원</h3>
        </>
      )}
    </div>
  );
}

실전 예제: 토글 스위치

import { useState } from 'react';

function ToggleSwitch() {
  const [isOn, setIsOn] = useState(false);

  const toggle = () => setIsOn(prev => !prev);

  return (
    <button
      onClick={toggle}
      style={{
        padding: '10px 20px',
        backgroundColor: isOn ? '#4CAF50' : '#ccc',
        color: 'white',
        border: 'none',
        borderRadius: '20px',
        cursor: 'pointer',
        transition: 'background-color 0.3s'
      }}
    >
      {isOn ? 'ON' : 'OFF'}
    </button>
  );
}

실행 결과:

꺼진 상태

켜진 상태

클릭할 때마다 ON/OFF가 토글되며 색상이 변경됩니다

State 사용 시 주의사항

1. 렌더링 중 State 변경 금지

// ❌ 무한 루프 발생!
function BadComponent() {
  const [count, setCount] = useState(0);
  setCount(count + 1);  // 렌더링할 때마다 실행됨!

  return <p>{count}</p>;
}

2. 조건문 안에서 useState 사용 금지

// ❌ Hooks 규칙 위반!
function BadComponent({ isLoggedIn }) {
  if (isLoggedIn) {
    const [user, setUser] = useState(null);
  }
  // ...
}

// ⭕ 항상 최상위에서 호출
function GoodComponent({ isLoggedIn }) {
  const [user, setUser] = useState(null);

  if (!isLoggedIn) {
    return <p>로그인해주세요</p>;
  }
  // ...
}

Hooks는 항상 컴포넌트 최상위에서, 같은 순서로 호출해야 해요.

3. State는 비동기로 업데이트됨

const handleClick = () => {
  setCount(count + 1);
  console.log(count);  // 이전 값이 출력됨!
};

State 업데이트 직후에 새 값을 쓰고 싶으면 useEffect를 써야 해요 (다음에 배울 거예요).

다음 단계

State 기초를 배웠으니, 다음 글에서는 State와 Props의 관계를 다뤄볼게요. 둘을 같이 쓰면 컴포넌트 간에 데이터를 주고받으면서 복잡한 앱도 만들 수 있어요.

State 연습을 많이 해보세요. 카운터, 토글, 폼 만들기 같은 간단한 것부터 시작하면 금방 익숙해져요!


React 시리즈 탐색:

← 블로그 목록으로