Skip to content
This page has been auto-translated and may contain errors.View in English

State

함수 안의 일반 변수는 함수가 끝나는 순간 사라지므로, 시간에 따라 변하는 값을 기억하고 화면에 표시할 수 없습니다. 이 간격을 채우는 것이 **state**입니다. 렌더링 사이에 컴포넌트가 뭔가를 기억하는 방식입니다. props는 외부에서 전달되어 컴포넌트가 절대 변경하지 않는 데이터인 반면, state는 컴포넌트가 소유하고 직접 업데이트하는 데이터입니다. useState 훅은 컴포넌트에 그 메모리를 제공하고, 값을 업데이트하면서 동시에 React에 다시 그리도록 신호하는 방법을 제공합니다.

카운터의 예시입니다:

jsx
import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

useState(0)은 시작값을 정하고 한 쌍을 돌려줍니다: 현재값 count와 이를 변경하는 함수 setCount. 이름은 자유이고, [value, setValue] 형태가 관례입니다. 버튼의 onClick은 클릭될 때마다 함수를 실행하는데, 이는 다음 Events 챕터에서 자세히 다루므로 지금은 그렇게 동작한다고 알아두세요. 그 함수는 setCount(count + 1)을 호출하며, 이 호출은 두 가지를 합니다. 새로운 값을 저장하고, Counter를 다시 렌더링해서 화면에 업데이트된 숫자를 표시합니다. setter를 호출하는 것은 뭔가 변했으니 컴포넌트를 다시 실행해야 한다는 신호를 React에 보내는 것입니다.

이전 값을 기반으로 업데이트하기

setter가 순수한 값 대신 함수와 함께 호출되는 것을 자주 보게 됩니다:

jsx
setCount(c => c + 1)

이것이 업데이터 형태입니다. React는 최신 값과 함께 함수를 호출하고 반환된 값을 다음 state로 사용합니다. 새로운 값이 이전 값에서 파생될 때, 특히 한 줄에서 여러 번 업데이트할 때 이 형태를 사용하세요. 이 두 줄은 보이는 것처럼 동작하지 않습니다:

jsx
setCount(count + 1)
setCount(count + 1)

두 번의 count 읽기 모두 이 렌더링에서의 같은 값을 보므로, 함께하면 count가 1만 증가합니다. 업데이터 형태는 이를 고칩니다. 각 호출이 이전 호출의 결과를 받기 때문입니다:

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

이제 count는 2씩 증가합니다. 불린을 토글하는 것도 같은 방식이며 업데이터 형태를 가장 자주 만나는 곳입니다: setIsOpen(open => !open)은 현재값을 받아 그 반대를 반환합니다. 다음 값이 현재값에 의존할 때, 업데이터 형태가 안전한 기본값입니다.

State는 컴포넌트 인스턴스별입니다

컴포넌트를 렌더링할 때마다 자신의 분리된 state를 갖습니다. 같은 페이지의 두 카운터는 각각 자신의 count를 유지합니다:

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

첫 번째 버튼을 클릭해도 두 번째에는 영향을 주지 않습니다. 같은 Counter 코드를 공유하지만 각각 독립적인 값을 유지합니다. state는 화면의 각 인스턴스에 속하므로, 하나의 함수가 많은 분리된 복사본을 동작시킬 수 있습니다.

State를 변경하지 말고 교체하세요

state를 업데이트하는 방법은 React에 새로운 값을 주는 것이며, React는 그 값을 이전 값과 비교해서 뭐가 변했는지 결정합니다. 기존 값을 제자리에서 변경하면 그 신호를 건너뛰므로 화면이 오래된 상태로 남습니다. 이는 객체와 배열에서 가장 중요합니다. 배열에 추가하려면, 기존 배열에 push하는 대신 새 배열을 만드세요:

jsx
// 업데이트를 건너뜀: React가 새로운 값을 본다
todos.push(newTodo)
setTodos(todos)

// 작동: 완전히 새로운 배열
setTodos(prev => [...prev, newTodo])

객체도 같습니다: 이전 객체를 새 객체로 spread하면서 변경사항을 적용하고, 그것을 업데이터에서 반환하세요. useState에서 받은 값을 읽기 전용으로 취급하고, 항상 setter에 새 것을 전달하세요.

중첩은 주의가 필요합니다. 객체를 spread하면 최상위만 복사되므로, 내부의 중첩 객체는 여전히 같은 객체이며, 그 중첩된 것을 변경하면 원본으로 돌아갑니다:

jsx
// 원본을 변경: 중첩 address 객체가 새 객체와 여전히 공유됨
user.address.city = 'Seoul'
setUser({ ...user })

// 작동: 변경까지의 모든 레벨에서 새로운 객체
setUser(prev => ({
  ...prev,
  address: { ...prev.address, city: 'Seoul' }
}))

객체 주변의 괄호는 화살표 함수가 블록 코드로 읽지 않고 반환하도록 만드는 것입니다. 변경까지의 모든 레벨이 자신의 spread를 필요로 하므로, 중첩이 깊어질수록 어색해집니다. 이런 체인을 작성하고 있다면, 보통 그 state를 더 작은 조각으로 평탄화하고 싶다는 신호입니다.

리스트의 한 항목 변경하기

이미 배열에 있는 항목을 변경하는 것이 사람들이 가장 자주 막히는 곳입니다. 이를 위한 도구는 map입니다. 배열을 순회하고, 원하는 항목에 대해 변경된 복사본을 반환하고, 다른 모든 항목은 그대로 반환하세요:

jsx
function toggleTodo(id) {
  setTodos(prev => prev.map(todo =>
    todo.id === id ? { ...todo, done: !todo.done } : todo
  ))
}

setter는 현재값에서 다음 배열을 만들어지므로 업데이터 형태를 취합니다. 삼항 연산자가 선택을 합니다: 일치하는 todo는 done이 뒤집혀진 복사본을 받고, 다른 모든 것은 그대로 통과합니다. 그 통과 항목들은 이전처럼 같은 객체이며, 이것은 괜찮습니다. 변경한 것과 그것을 담는 배열만 새로워야 하며, map이 setter를 위해 그 새로운 배열을 만듭니다.

항목 제거는 filter이고, 하나를 추가하는 것은 이전 섹션의 spread입니다. 이 세 가지 사이에서 대부분의 리스트 업데이트가 커버됩니다.

초기값이 비쌀 때

JavaScript는 useState에 전달하는 인수를 모든 렌더링에서 useState 자체가 실행되기 전에 평가하고, React는 첫 번째 시간에만 결과를 유지합니다. 0 같은 값의 경우 비용이 들지 않습니다. 실제 작업을 하는 함수 호출의 경우, 모든 렌더링에서 실행되고 결과는 첫 번째 이후 버려집니다:

jsx
// buildBoard()가 모든 렌더링에서 실행되고 결과는 첫 번째 이후 폐기됨
const [board, setBoard] = useState(buildBoard())

// buildBoard는 첫 번째 렌더링에서 한 번 호출됨
const [board, setBoard] = useState(buildBoard)

첫 번째 줄은 함수를 호출하고 React에 결과를 전달합니다. 두 번째 줄은 React에 함수 자체를 전달하고 React가 한 번 호출하도록 합니다. 저장소 읽기, 큰 구조 생성, 무거운 계산 같이 진정으로 비용이 많이 드는 작업을 위해 이것을 아껴두세요. useState(0)에서는 소음일 뿐입니다.

Version note

클래스 컴포넌트에서는 state가 this.state에 살았고 this.setState로 업데이트했습니다. useState는 함수 컴포넌트의 동등품이며 현대 React에서 state를 유지하는 표준 방법입니다.

몇 가지 실용적인 습관이 여기서 당신을 도와줍니다. 다음 값이 현재값에 기반할 때마다 업데이터 형태(setCount(c => c + 1))를 기본값으로 사용하고, 이전 값을 무시하고 새로운 값을 설정할 때 순수 형태(setCount(0))를 사용하세요. 이 하나의 규칙이 대부분의 타이밍 놀라움을 피하도록 해줍니다.

state도 최소한으로 유지하세요. 이미 가지고 있는 것에서 계산할 수 없는 것만 저장하세요. state에 항목의 리스트를 유지하면, 그 개수도 state에 유지하지 마세요; 렌더링하면서 items.length로 읽으세요. 모든 추가 state 조각은 동기화를 유지해야 하는 또 다른 값이며, 중복 state가 정확히 두 복사본이 벗어나는 곳입니다. 여러 값이 항상 함께 변할 때, 분산된 여러 useState 호출보다는 하나의 객체나 useReducer 훅이 더 안정적입니다.

state를 스냅샷으로 생각하면 도움이 됩니다. 어떤 단일 렌더링에 대해, count는 고정 값이며, 그 렌더링이 시작될 때 결정되고 실행되는 동안 변경되지 않습니다. setCount는 그 변수를 재할당하지 않습니다; 다시 렌더링하도록 React에 요청하고, 새 값은 다음 렌더링의 스냅샷에만 나타납니다. 설정 직후에 state를 읽으면 이전 값을 얻습니다:

jsx
function handleClick() {
  setCount(count + 1)
  console.log(count) // 여전히 이 렌더링의 값이고, 새로운 것이 아님
}

이것이 또한 stale closure가 오는 곳입니다. 렌더링 중에 정의하는 모든 함수, 이벤트 핸들러, effects, 타이머는 그 렌더링의 state 값을 캡처합니다. 그 중 하나가 나중에 실행되면, 예를 들어 setTimeout 콜백이면, 여전히 캡처한 count를 보며, 값이 지나갔음에도 불구하고 그렇습니다. 업데이터 형태는 이를 피합니다. setCount(c => c + 1)은 캡처된 것에 의존하는 대신 실행되는 순간의 현재 값을 React에 요청하기 때문입니다.

배칭은 그림의 다른 절반입니다. React는 단일 이벤트 핸들러 내부에서 일어나는 state 업데이트를 그룹화하고 각 setCount 후가 아니라 핸들러가 끝난 후에 한 번 다시 렌더링합니다. 이것이 count + 1로 setter를 두 번 호출해도 값이 1만 증가하는 이유입니다: 모든 호출이 어떤 다시 렌더링도 일어나기 전에 같은 스냅샷에 대해 큐에 들어가기 때문입니다. 대조적으로 업데이터 함수는 큐에 들어가고 서로에 대해 순서대로 실행되므로 구성됩니다. React 18부터 이 배칭은 promise, timeout, 다른 async 콜백 내부의 업데이트로 확장되므로, 같은 규칙이 순수 이벤트 핸들러 이상으로 잘 적용됩니다.

JunoState는 컴포넌트의 메모리 일반 변수는 함수가 실행될 때마다 그 값을 잊지만, useState는 그것을 기억합니다. 값과 setter를 얻고, 그 setter를 호출하면 새로운 값을 저장하고 컴포넌트를 다시 그립니다. 새로운 값이 이전 값에 기반할 때, 항상 최신값에서 작업하도록 setCount(c => c + 1)을 사용하세요. 그 값이 배열이나 객체일 때, setter에 변경사항이 적용된 완전히 새로운 것을 제공하세요.
JunoState는 컴포넌트의 메모리useState는 값과 setter를 제공하며, setter를 호출하는 것이 다시 렌더링을 트리거하는 것입니다. 다음 값이 현재값에 의존하거나 여러 번 업데이트할 때 업데이터 형태를 사용하세요. State는 인스턴스별이므로, 같은 컴포넌트 두 개는 분리된 값을 유지하고, 항상 이전 배열이나 객체를 변경하는 대신 setter에 새로운 값을 전달합니다. 리스트의 경우, 그것은 map을 의미합니다: 변경하는 항목의 새로운 복사본, 다른 모든 것은 건드리지 않습니다.
JunoState는 컴포넌트의 메모리 state를 스냅샷으로 취급하세요: 렌더링 기간 동안 고정되고, 다음 렌더링에서만 업데이트되므로, 설정 직후에 읽으면 이전 값을 얻고 캡처된 closure는 stale이 됩니다. React는 이벤트의 업데이트를 배치하고 한 번 다시 렌더링하므로, 같은 스냅샷에 대한 setter 호출은 쌓이지 않지만, 업데이터 함수는 쌓입니다. 정확성이 이전 값에 의존할 때, 업데이터 형태를 잡으세요. 그리고 시작값이 구축하기에 비쌀 때, useState에 함수 자체를 전달해서 React가 한 번 실행하도록 하세요.

다음: Events, 이제까지 작성한 onClick 핸들러가 제대로 살펴봅니다.