useState는 컴포넌트가 렌더링 사이에 무언가를 기억하게 해 주는 훅입니다. 카운트, 입력창의 텍스트, 메뉴가 열려 있는지 여부 같은 것들입니다. setter를 호출할 때마다 React는 새 값으로 컴포넌트를 다시 렌더링합니다.
버튼을 클릭한 다음, 에디터에서 useState(0)을 useState(10)으로 바꿔 보세요. 미리보기가 다시 만들어지고 카운트가 10부터 시작합니다.
문법
const [value, setValue] = useState(initialValue);
initialValue는 첫 번째 렌더링에서만 사용됩니다. 이후 렌더링에서는 React가 이 값을 무시하고 저장해 둔 값을 돌려줍니다.value는 이번 렌더링의 현재 값입니다.setValue는 값을 교체하고 새 렌더링을 예약합니다.
이름은 자유롭게 정할 수 있습니다. useState는 두 항목짜리 배열을 반환하고 대괄호가 그 배열을 풀어 주므로, [isOpen, setIsOpen]과 [email, setEmail]은 같은 패턴입니다.
useState는 컴포넌트의 최상위에서 호출해야 하며, 반복문, 조건문, 중첩 함수 안에서는 절대 호출하지 마세요. React는 호출 순서로 각 호출과 저장된 값을 짝지으므로, 그 순서가 렌더링마다 같아야 합니다(훅의 규칙 참고).
여러 개의 상태
컴포넌트는 필요한 만큼 useState를 여러 번 호출할 수 있습니다. 각 호출은 서로 독립적인 값입니다.
상태는 스냅샷입니다
setter는 손에 쥔 변수를 바꾸지 않습니다. 다음 렌더링에서 값이 무엇이어야 하는지를 React에 알려 줄 뿐입니다. 그 렌더링이 일어나기 전까지 현재 렌더링은 계속 이전 값을 봅니다.
미리보기 아래의 콘솔을 열고 한 번 클릭해 보세요. 핸들러는 이전 값인 0을 출력하고, 그다음 컴포넌트가 1로 새로 렌더링되었다는 로그를 남깁니다. 잘못된 것은 없습니다. count는 렌더링 전체에서 상수이고, 다음 렌더링은 새로운 값을 받습니다.
업데이터 함수
스냅샷이기 때문에 count + 1로 setter를 세 번 호출하면 3이 아니라 1만 더해집니다. 모든 호출이 0 + 1을 계산하기 때문입니다. 대신 함수를 넘기면 React가 각 호출에 이전 호출의 결과를 전달합니다.
다음 값이 이전 값에 의존하고, 업데이트가 최신 렌더링에서 곧바로 실행되지 않을 수 있을 때는 항상 업데이터 형태를 사용하세요. 한 핸들러 안에서 여러 번 설정할 때, setInterval 콜백, 이펙트 안이 그런 경우입니다.
상태 안의 객체와 배열
React는 이전 값과 새 값을 Object.is로 비교해서 다시 렌더링할지 결정합니다. 객체를 직접 변경하고 같은 객체를 다시 넘기면 비교 결과는 바뀐 것이 없다고 판단하고, 화면은 그대로 남습니다. 항상 새 객체나 새 배열을 넘기세요.
{ ...user, age: user.age + 1 }는 모든 필드를 복사하고 하나만 덮어씁니다. [...tags, 'code']는 항목을 추가하고 filter는 항목을 제거하며, 둘 다 새 배열을 반환합니다. 배열과 객체 업데이트 페이지에서 중첩 객체, 목록의 항목 하나 수정하기, 중간에 삽입하기를 다룹니다.
지연 초기화
useState의 인자는 React가 처음에만 사용하더라도 렌더링마다 평가됩니다. 그 값을 계산하는 비용이 클 때(localStorage 읽기, 큰 문자열 파싱 등)는 함수를 넘기세요. React는 첫 번째 렌더링에서 한 번만 호출합니다.
// Runs readSavedNotes() on every render, then throws the result away
const [notes, setNotes] = useState(readSavedNotes());
// Runs it once
const [notes, setNotes] = useState(readSavedNotes);
const [notes, setNotes] = useState(() => readSavedNotes());
key로 상태 초기화하기
상태는 트리 안에서 컴포넌트의 위치에 속합니다. 같은 위치에 다른 key를 렌더링하면 React는 새 컴포넌트로 취급하고 상태가 처음부터 다시 시작됩니다. 폼이 편집하는 대상이 바뀔 때 폼을 초기화하는 가장 깔끔한 방법입니다.
메시지를 입력한 다음 받는 사람을 바꿔 보세요. 초안이 지워집니다. key={to}를 지우고 다시 해 보면, React가 같은 위치의 같은 Draft로 보기 때문에 텍스트가 남아 있습니다.
자주 하는 실수
상태를 설정한 직후에 로그 출력하기. 위에서 본 것처럼 이전 값이 보입니다. 컴포넌트 본문에서 로그를 찍거나, 다음 값을 먼저 변수에 계산해 두고 그 변수를 사용하세요.
상태를 직접 변경하기. user.age++; setUser(user)는 같은 객체를 넘기므로 아무것도 렌더링되지 않습니다. 먼저 복사하세요.
렌더링 중에 setter 호출하기. <button onClick={setCount(count + 1)}>은 렌더링하는 동안 setCount를 호출하고, 그러면 다시 렌더링되고, 또다시 호출됩니다. React는 "Too many re-renders"로 이 반복을 멈춥니다. 함수를 넘기세요: onClick={() => setCount(count + 1)}.
계산할 수 있는 값을 저장하기. fullName이 항상 first + ' ' + last라면, 어긋날 수 있는 세 번째 상태를 두지 말고 렌더링 중에 계산하세요.
자주 묻는 질문
useState는 무엇을 반환하나요?
정확히 두 개의 항목을 가진 배열을 반환합니다. 현재 값과 그 값을 교체하는 함수입니다. 이름은 배열 구조 분해로 직접 정하며, 관례상 [thing, setThing] 형태를 씁니다.
상태가 바로 업데이트되지 않는 이유는 무엇인가요?
setter를 호출해도 이미 가지고 있는 변수는 바뀌지 않습니다. setter는 React에 컴포넌트를 다시 렌더링하라고 요청하고, 새 값은 다음 렌더링에서 나타납니다. 현재 이벤트 핸들러 안에서는 이전 값이 그대로 있으므로 setCount(count + 1) 직후의 console.log(count)는 이전 숫자를 출력합니다.
setter에 함수를 넘겨야 하는 경우는 언제인가요?
새 값이 이전 값에 따라 달라지고, 여러 번 설정하거나 타이머 또는 이펙트 안에서 설정할 때입니다. setCount(c => c + 1)은 항상 최신 값에서 시작하지만, setCount(count + 1)은 이번 렌더링이 본 값에서 시작합니다.
useState에 객체나 배열을 저장할 수 있나요?
네, 하지만 읽기 전용으로 다뤄야 합니다. 새 객체나 배열을 만들어 setter에 넘기세요(setUser({ ...user, name }), setItems([...items, item])). 기존 객체를 직접 변경하면 렌더링이 일어나지 않습니다.
useState와 useReducer의 차이는 무엇인가요?
둘 다 상태를 저장합니다. useState는 서로 독립적인 값 몇 개에 적합합니다. useReducer는 업데이트 로직을 액션을 받는 하나의 함수로 옮기므로, 여러 이벤트가 같은 상태를 연관된 방식으로 바꿀 때 더 읽기 쉽습니다.