React 실무 가이드 · Part 3

state와 렌더링

React에서 가장 많은 버그가 나오는 영역을 원리로 이해하기

작성 기준2026년 7월버전과 지원 현황은 이후 달라질 수 있으니 공식 문서를 함께 확인하세요.

이 파트에서 다루는 내용

useState 기본렌더링이 일어나는 시점불변성상태 위치 결정
01

state는 렌더링 사이에 값을 기억합니다

컴포넌트는 화면을 그릴 때마다 함수가 처음부터 다시 실행됩니다. 일반 변수는 매번 초기화되므로 값을 유지할 수 없습니다.

state는 React가 컴포넌트 바깥에서 관리하는 값입니다. 그래서 함수가 다시 실행돼도 이전 값이 유지되고, state를 바꾸면 React가 화면을 다시 그립니다.

일반 변수와 state의 차이jsx
function Counter() {
  let count = 0;              // 렌더링마다 0으로 초기화됩니다
  const [value, setValue] = useState(0);   // 값이 유지됩니다

  return (
    <div>
      <button onClick={() => { count += 1; }}>
        일반 변수 {count}   {/* 화면이 바뀌지 않습니다 */}
      </button>
      <button onClick={() => setValue(value + 1)}>
        state {value}      {/* 화면이 갱신됩니다 */}
      </button>
    </div>
  );
}

값이 바뀌어도 화면을 다시 그릴 필요가 없다면 state가 아니어도 됩니다. 화면에 반영돼야 하는 값만 state로 둡니다.

02

상태 변경은 즉시 반영되지 않습니다

set 함수를 호출해도 그 줄 다음에서 값이 바로 바뀌지는 않습니다. React는 변경 요청을 모아 두었다가 렌더링을 한 번만 수행합니다.

이 동작을 모르면 '분명 두 번 더했는데 1만 올라간다' 같은 문제를 만납니다.

렌더링이 일어나는 경우
정리
  • 자신의 state가 바뀔 때
  • 부모가 다시 렌더링될 때
  • 구독 중인 context 값이 바뀔 때
같은 값이면 건너뜁니다
동작

set 함수에 현재와 같은 값을 넣으면 React가 렌더링을 생략합니다. 다만 객체는 내용이 같아도 새로 만들면 다른 값으로 취급됩니다.

값 기준과 함수 기준jsx
// 의도와 다르게 1만 증가합니다
function increaseTwice() {
  setCount(count + 1);   // count는 이번 렌더링 시점의 값입니다
  setCount(count + 1);   // 같은 값을 기준으로 계산합니다
}

// 이전 값을 기준으로 계산해 2가 증가합니다
function increaseTwiceCorrect() {
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1);
}

이전 값을 바탕으로 새 값을 만들 때는 함수를 넘기는 방식을 씁니다. 비동기 처리 후 상태를 갱신할 때도 마찬가지입니다.

03

state는 새로 만들어 교체합니다

배열이나 객체 state를 다룰 때 기존 값을 직접 수정하면 React가 변화를 감지하지 못합니다. 참조가 같기 때문입니다.

항상 새 객체나 새 배열을 만들어 교체해야 합니다. 이것이 불변성 규칙입니다.

직접 수정과 새로 만들기jsx
// 화면이 갱신되지 않습니다
items.push(newItem);
setItems(items);

user.name = "홍길동";
setUser(user);

// 새로 만들어 교체합니다
setItems([...items, newItem]);                       // 추가
setItems(items.filter((item) => item.id !== id));    // 삭제
setItems(items.map((item) =>                         // 수정
  item.id === id ? { ...item, done: true } : item
));

setUser({ ...user, name: "홍길동" });                 // 객체 수정

중첩이 깊어지면 펼치는 코드가 길어집니다. 그럴 때는 상태 구조 자체가 너무 복잡한 것은 아닌지 먼저 검토합니다.

04

계산할 수 있는 값은 state로 두지 않습니다

기존 state로 계산해 낼 수 있는 값을 별도 state로 두면 두 값을 항상 맞춰야 합니다. 한쪽만 갱신되는 순간 화면이 어긋납니다.

state로 둘 값
기준
  • 사용자 입력값
  • 서버에서 받아온 원본 데이터
  • 열림·닫힘 같은 UI 상태
state로 두지 않을 값
기준
  • 다른 state로 계산 가능한 값
  • props로 받은 값을 그대로 복사한 값
  • 화면에 영향을 주지 않는 값
불필요한 state 제거jsx
// 문제: items가 바뀔 때마다 totalCount도 따로 맞춰야 합니다
const [items, setItems] = useState([]);
const [totalCount, setTotalCount] = useState(0);

// 개선: 렌더링할 때 계산합니다
const [items, setItems] = useState([]);
const totalCount = items.length;
const totalPrice = items.reduce((sum, item) => sum + item.price, 0);

계산 비용이 정말 큰 경우에만 메모이제이션을 검토합니다. 대부분의 계산은 그냥 해도 충분히 빠릅니다. Part 6에서 다룹니다.

05

상태는 필요한 곳 중 가장 가까운 위치에 둡니다

두 컴포넌트가 같은 값을 공유해야 하면 공통 부모로 올립니다. 이것을 상태 끌어올리기라고 합니다.

반대로 한 컴포넌트만 쓰는 값을 위로 올리면 관계없는 컴포넌트까지 다시 렌더링됩니다. 필요 이상으로 올리지 않는 것도 중요합니다.

아래에 둘 때
지역 상태

한 컴포넌트 안에서만 쓰는 값은 그 안에 둡니다. 열림 여부, 입력 중인 값 등이 해당합니다.

위로 올릴 때
공유 상태

형제 컴포넌트가 같은 값을 봐야 하면 공통 부모로 올리고 props로 내려보냅니다.

전역으로 갈 때
Part 8

여러 단계를 거쳐 내려보내야 하거나 앱 전체에서 필요하면 전역 상태를 검토합니다. 다만 성급하게 전역으로 올리지 않습니다.

판단 순서

지역 상태로 시작해서 필요할 때만 올립니다. 처음부터 전역 상태로 만들면 어디서 값이 바뀌는지 추적하기 어려워지고, 대부분의 화면은 지역 상태만으로 충분합니다.

버전

버전별 참고

본문은 React 19 기준입니다. 18과 달라지는 부분은 아래에 정리합니다.

  • React 18부터 이벤트 핸들러 바깥의 상태 변경도 자동으로 묶여 처리됩니다. 비동기 응답 후 여러 상태를 바꿔도 렌더링이 한 번만 일어납니다.
  • React 19에서는 폼 처리와 비동기 전환을 위한 훅이 추가됐습니다. 기본적인 useState 사용 방식은 동일합니다.
체크

이 파트 완료 기준