학습 자료

state — 화면이 바뀌는 진짜 이유


Article

1강에서 UI = f(state)라고 했다. 이제 그 state가 무엇인지 볼 차례다.

버튼을 누르면 숫자가 올라가는 카운터를 만들어본다. 배운 것만으로 써보자.

그냥 변수로 해본다

function Counter() {
  let count = 0;

  function handleClick() {
    count++;
    console.log(count);
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  );
}

버튼을 세 번 눌러본다.

콘솔
  • 1
  • 2
  • 3

콘솔에는 값이 올라간다. 그런데 화면은 계속 0이다.

왜 화면이 안 바뀌나

두 가지 문제가 겹쳤다.

무슨 일이 벌어졌나
  1. 01

    React 는 다시 그릴 이유를 모른다

    count++ 은 그냥 자바스크립트 변수를 바꿨을 뿐이다. React 에게 아무도 알려주지 않았다.

  2. 02

    설령 다시 그려도 값이 초기화된다

    컴포넌트는 함수다. 다시 실행되면 let count = 0 이 또 실행된다.

    let count = 0;  // 매번 0 으로 시작

첫 번째가 알림의 문제, 두 번째가 기억의 문제다.

컴포넌트 바깥에 두면?

기억 문제를 피하려고 함수 밖으로 빼본다.

let count = 0;

function Counter() {
  function handleClick() {
    count++;
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  );
}

값은 남는다. 하지만 여전히 화면은 안 바뀐다. React가 모르기 때문이다.

게다가 더 나쁜 문제가 생긴다.

function App() {
  return (
    <>
      <Counter />
      <Counter />
    </>
  );
}

카운터 두 개가 같은 count를 공유한다. 한쪽을 누르면 다른 쪽도 올라가야 하는데, 그마저도 화면에 반영되지 않는다.

변수가 실패하는 이유

함수 안의 변수

let count = 0

  • 다시 실행되면 초기화된다
  • 값을 기억하지 못한다
  • React 가 알아채지 못한다

함수 밖의 변수

모듈 최상단

  • 값은 남는다
  • 모든 인스턴스가 공유한다
  • React 가 알아채지 못한다

필요한 건 각 컴포넌트마다 따로 기억되면서, 바뀌면 React가 알아채는 값이다. 그게 state다.

useState

import { useState } from "react";

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

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  );
}

이제 화면이 바뀐다.

useState는 값을 두 개 담은 배열을 돌려주고, 우리는 자바스크립트 편 18강의 배열 구조분해로 꺼낸다.

이름무엇인가
[0]count지금의 값 (읽기 전용)
[1]setCount값을 바꾸고 React 에게 알리는 함수
인자useState(0)첫 렌더링에서만 쓰이는 초기값

이름은 관례다. [값, set값] 형태로 짓는다.

const [name, setName] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [items, setItems] = useState([]);

setState 가 하는 두 가지 일

setCount(1)은 값을 바꾸는 게 전부가 아니다.

setCount 를 부르면
  1. 값을 저장한다React 가 따로 보관
  2. 리렌더링 예약"이 컴포넌트 다시 그려"
  3. 함수 재실행새 count 로 화면을 만든다

두 번째가 핵심이다. 값만 바꾸는 게 아니라 React에게 "다시 그려달라"고 알린다. 그냥 변수에는 없던 기능이다.

그리고 값은 React가 컴포넌트 바깥 어딘가에 보관한다. 그래서 함수가 다시 실행돼도 사라지지 않는다.

값을 누가 들고 있나
  • React 내부컴포넌트마다 따로 보관한다
    • Counter (첫 번째)count: 3
      • 다시 실행돼도 3 을 돌려준다
    • Counter (두 번째)count: 0 — 완전히 별개
      • 서로 간섭하지 않는다

컴포넌트를 두 개 쓰면 state도 두 개다. 자바스크립트 편 14강의 클로저에서 makeCounter()를 두 번 부르면 count가 각자 생기던 것과 같은 이야기다.

state 는 그 렌더링의 스냅샷이다

가장 헷갈리는 부분이다. 이 코드는 몇이 될까.

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

  function handleClick() {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  }

  return <button onClick={handleClick}>{count}</button>;
}

세 번 더했으니 3일 것 같다. 1이다.

왜 1 인가
  1. 01

    이 렌더링에서 count 는 0 이다

    count 는 변수다. 함수가 실행되는 동안 바뀌지 않는다.

  2. 02

    세 줄 모두 setCount(0 + 1) 이다

    count 가 계속 0 이므로 전부 setCount(1) 이다.

    setCount(0 + 1);  // 세 번 다 같다
  3. 03

    React 는 마지막 값을 쓴다

    리렌더링 후 count1 이다.

setCount를 불러도 지금 실행 중인 함수의 count는 안 바뀐다. 다음 렌더링에서야 새 값이 들어온다.

function handleClick() {
  console.log(count);
  setCount(count + 1);
  console.log(count);
}
콘솔
  • 0
  • 0

연달아 더하려면 함수를 넘긴다.

setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);

이러면 3이 된다. React가 이전 값을 인자로 넣어주기 때문이다. 다음 강의에서 자세히 본다.

무엇이 state 여야 하나

전부 state로 만들면 안 된다. 판단 기준이 있다.

state 여야 하는가

state 로

시간에 따라 변하고, 변하면 화면이 바뀐다

  • 입력창에 친 글자
  • 모달이 열렸는지
  • 서버에서 받아온 목록
  • 지금 선택된 탭

state 가 아님

다른 값으로 계산되거나, 안 변한다

  • props 로 받은 값
  • 다른 state 로 계산되는 값
  • 화면과 무관한 값
  • 상수

계산할 수 있는 값을 state로 두면 반드시 어긋난다.

function Cart({ items }) {
  const [total, setTotal] = useState(0);
  // items 가 바뀔 때마다 setTotal 을 부르는 걸 잊으면 끝
}

그냥 계산한다.

function Cart({ items }) {
  const total = items.reduce((acc, item) => acc + item.price, 0);
  return <p>{total}원</p>;
}

items가 바뀌면 컴포넌트가 다시 실행되고, total도 자동으로 다시 계산된다. 1강에서 말한 "데이터와 화면이 어긋나지 않는다"가 이것이다.

정리하면

  • 그냥 변수는 다시 실행되면 초기화되고, 모듈 변수는 모든 인스턴스가 공유한다. 둘 다 React가 알아채지 못한다.
  • useState는 값을 기억하고, 바뀌면 리렌더링을 예약한다.
  • state는 컴포넌트마다 따로 보관된다.
  • count는 그 렌더링의 스냅샷이다. setCount를 불러도 지금 함수 안의 값은 안 바뀐다.
  • 연달아 갱신하려면 함수를 넘긴다. setCount(c => c + 1)
  • 계산할 수 있는 값은 state로 두지 않는다. state가 적을수록 어긋나지 않는다.

다음 강의에서는 useState를 제대로 다룬다. 갱신 함수, 배칭, 객체와 배열을 담을 때의 규칙까지.

state — 화면이 바뀌는 진짜 이유 — 디코드랩(DCODELAB)