C.W.K.
Stream
Lesson 01 of 07 · published

useState — 대부분의 시간에 유일하게 필요한 state hook

~14 min · usestate, state, fundamentals

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
useState 는 작아. 주변 함정은 아니야. 이 레슨이 함정 지도.

모양

const [value, setValue] = useState(initialValue). Hook 이 [현재 값, setter 함수] 튜플 반환. 현재 값은 매 render 마다 fresh. Setter 는 안정적 — render 간 identity 안 바뀜, dependency array 에 넣어도 재실행 안 트리거.

Lazy initializer

initialValue 계산 비싸면 함수 형태로: useState(() => computeIt()). React 가 초기 render 에 한 번 호출하고 영원히 무시. 평범한 호출 형태 useState(computeIt()) 는 React 가 첫 호출 결과만 써도 매 render 마다 계산 돔.

Functional update

새 값이 이전 값에 의존하면 setter 에 함수 넘겨: setCount(n => n + 1). Stale-closure 버그 (이벤트 핸들러가 옛 값 잡고 캡처 사이에 다른 업데이트 land) 막아. 새 값이 옛 값에 의존하면 항상 functional 형태 써.

State batching

React 19 가 같은 이벤트 핸들러, effect, async 함수 호출 안에서 일어나는 state 업데이트 다 배치. 클릭 핸들러의 setA(1); setB(2); 가 re-render 하나 트리거 — 둘 아님. 보통 신경 안 써도 됨 — 다만 업데이트 두 개가 render 하나 만들어서 놀라면 배칭이 이유.

Object 와 array — immutable update 만

State 가 reference equality 로 업데이트. 같은 객체 reference 로 setUser(user) 는 no-op. Object 필드 업데이트는 새 object 로 spread: setUser({ ...user, name: 'new' }). Array 도 마찬가지: setItems([...items, newItem]). 기존 object/array mutate 하고 setter 호출하면 re-render 안 트리거.

useState 여러 개 vs 큰 object 하나

두 state 값이 같이 바뀌면 묶어: useState({ x: 0, y: 0 }). 독립적으로 바뀌면 분리: useState(0); useState(0). 경험칙: 항상 같이 업데이트 되는 state 는 같이; 독립적으로 업데이트 되는 건 분리.

파생 state 는 state 아냐. fullNamefirstName + lastName 에서 계산되면 useState 에 저장 마. Render 에서 계산: const fullName = `${firstName} ${lastName}`. 파생 값 저장이 state-sync 버그의 시작.

Code

useState — 흔한 네 모양·tsx
import { useState } from "react";

export function Examples() {
  // 1. 단순 scalar
  const [count, setCount] = useState(0);

  // 2. Lazy initializer — 비싼 계산 한 번만 돔
  const [tree, setTree] = useState(() => buildTreeFromLocalStorage());

  // 3. Functional update — 배칭과 stale closure 에 안전
  const increment = () => setCount((n) => n + 1);

  // 4. Object state — immutable 업데이트
  const [user, setUser] = useState({ name: "", email: "" });
  const updateName = (name: string) => setUser((u) => ({ ...u, name }));

  return (
    <div>
      <button onClick={increment}>Count: {count}</button>
      <input
        value={user.name}
        onChange={(e) => updateName(e.target.value)}
      />
    </div>
  );
}

function buildTreeFromLocalStorage() { return { nodes: [] }; }
Stale-closure 함정·tsx
// WRONG — 빠른 클릭 셋이 3 아니라 1 증가.
function Broken() {
  const [count, setCount] = useState(0);
  const onClick = () => {
    setCount(count + 1);  // render 시점의 count 캡처
    setCount(count + 1);  // 같은 캡처된 count
    setCount(count + 1);  // 같은 캡처된 count
  };
  return <button onClick={onClick}>Count: {count}</button>;
}

// RIGHT — functional update 가 올바르게 합성.
function Working() {
  const [count, setCount] = useState(0);
  const onClick = () => {
    setCount((n) => n + 1);
    setCount((n) => n + 1);
    setCount((n) => n + 1);
  };
  return <button onClick={onClick}>Count: {count}</button>;
}

External links

Exercise

단일 object state { title: string; body: string; tags: string[] } 가진 <DraftPost /> 컴포넌트 빌드. title + body 입력, 태그 추가 버튼, index 로 태그 제거 버튼 제공. 그 다음 useState 호출 셋으로 리팩토링. 이 경우 어느 버전이 더 명확하게 읽혀? (함정 질문 — 무관 필드엔 분리가 보통 이김; 강결합 필드엔 그룹이 이김. 가능한 직관 형성.)
Hint
Tags array 업데이트: 추가는 setTags((tags) => [...tags, newTag]), 제거는 setTags((tags) => tags.filter((_, i) => i !== removeIdx)). 절대 in-place mutate 안 함.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.