State
함수 안의 일반 변수는 함수가 끝나는 순간 사라지므로, 시간에 따라 변하는 값을 기억하고 화면에 표시할 수 없습니다. 이 간격을 채우는 것이 **state**입니다. 렌더링 사이에 컴포넌트가 뭔가를 기억하는 방식입니다. props는 외부에서 전달되어 컴포넌트가 절대 변경하지 않는 데이터인 반면, state는 컴포넌트가 소유하고 직접 업데이트하는 데이터입니다. useState 훅은 컴포넌트에 그 메모리를 제공하고, 값을 업데이트하면서 동시에 React에 다시 그리도록 신호하는 방법을 제공합니다.
카운터의 예시입니다:
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가 순수한 값 대신 함수와 함께 호출되는 것을 자주 보게 됩니다:
setCount(c => c + 1)이것이 업데이터 형태입니다. React는 최신 값과 함께 함수를 호출하고 반환된 값을 다음 state로 사용합니다. 새로운 값이 이전 값에서 파생될 때, 특히 한 줄에서 여러 번 업데이트할 때 이 형태를 사용하세요. 이 두 줄은 보이는 것처럼 동작하지 않습니다:
setCount(count + 1)
setCount(count + 1)두 번의 count 읽기 모두 이 렌더링에서의 같은 값을 보므로, 함께하면 count가 1만 증가합니다. 업데이터 형태는 이를 고칩니다. 각 호출이 이전 호출의 결과를 받기 때문입니다:
setCount(c => c + 1)
setCount(c => c + 1)이제 count는 2씩 증가합니다. 불린을 토글하는 것도 같은 방식이며 업데이터 형태를 가장 자주 만나는 곳입니다: setIsOpen(open => !open)은 현재값을 받아 그 반대를 반환합니다. 다음 값이 현재값에 의존할 때, 업데이터 형태가 안전한 기본값입니다.
State는 컴포넌트 인스턴스별입니다
컴포넌트를 렌더링할 때마다 자신의 분리된 state를 갖습니다. 같은 페이지의 두 카운터는 각각 자신의 count를 유지합니다:
function App() {
return (
<>
<Counter />
<Counter />
</>
)
}첫 번째 버튼을 클릭해도 두 번째에는 영향을 주지 않습니다. 같은 Counter 코드를 공유하지만 각각 독립적인 값을 유지합니다. state는 화면의 각 인스턴스에 속하므로, 하나의 함수가 많은 분리된 복사본을 동작시킬 수 있습니다.
State를 변경하지 말고 교체하세요
state를 업데이트하는 방법은 React에 새로운 값을 주는 것이며, React는 그 값을 이전 값과 비교해서 뭐가 변했는지 결정합니다. 기존 값을 제자리에서 변경하면 그 신호를 건너뛰므로 화면이 오래된 상태로 남습니다. 이는 객체와 배열에서 가장 중요합니다. 배열에 추가하려면, 기존 배열에 push하는 대신 새 배열을 만드세요:
// 업데이트를 건너뜀: React가 새로운 값을 본다
todos.push(newTodo)
setTodos(todos)
// 작동: 완전히 새로운 배열
setTodos(prev => [...prev, newTodo])객체도 같습니다: 이전 객체를 새 객체로 spread하면서 변경사항을 적용하고, 그것을 업데이터에서 반환하세요. useState에서 받은 값을 읽기 전용으로 취급하고, 항상 setter에 새 것을 전달하세요.
중첩은 주의가 필요합니다. 객체를 spread하면 최상위만 복사되므로, 내부의 중첩 객체는 여전히 같은 객체이며, 그 중첩된 것을 변경하면 원본으로 돌아갑니다:
// 원본을 변경: 중첩 address 객체가 새 객체와 여전히 공유됨
user.address.city = 'Seoul'
setUser({ ...user })
// 작동: 변경까지의 모든 레벨에서 새로운 객체
setUser(prev => ({
...prev,
address: { ...prev.address, city: 'Seoul' }
}))객체 주변의 괄호는 화살표 함수가 블록 코드로 읽지 않고 반환하도록 만드는 것입니다. 변경까지의 모든 레벨이 자신의 spread를 필요로 하므로, 중첩이 깊어질수록 어색해집니다. 이런 체인을 작성하고 있다면, 보통 그 state를 더 작은 조각으로 평탄화하고 싶다는 신호입니다.
리스트의 한 항목 변경하기
이미 배열에 있는 항목을 변경하는 것이 사람들이 가장 자주 막히는 곳입니다. 이를 위한 도구는 map입니다. 배열을 순회하고, 원하는 항목에 대해 변경된 복사본을 반환하고, 다른 모든 항목은 그대로 반환하세요:
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 같은 값의 경우 비용이 들지 않습니다. 실제 작업을 하는 함수 호출의 경우, 모든 렌더링에서 실행되고 결과는 첫 번째 이후 버려집니다:
// 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를 유지하는 표준 방법입니다.
useState는 그것을 기억합니다. 값과 setter를 얻고, 그 setter를 호출하면 새로운 값을 저장하고 컴포넌트를 다시 그립니다. 새로운 값이 이전 값에 기반할 때, 항상 최신값에서 작업하도록 setCount(c => c + 1)을 사용하세요. 그 값이 배열이나 객체일 때, setter에 변경사항이 적용된 완전히 새로운 것을 제공하세요. 다음: Events, 이제까지 작성한 onClick 핸들러가 제대로 살펴봅니다.

