컴포넌트에 state 변수를 추가할 수 있는 React Hook
const [state, setState] = useState(initialState);
컴포넌트의 최상위 레벨에서 useState를 호출하여 state 변수를 선언할 수 있음.
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(28);
const [name, setName] = useState('Taylor');
const [todos, setTodos] = useState(() => createTodos());
// ...
array destructuring을 사용하여 [something, setSomething]과 같이 state 변수의 이름을 지정하는 것이 관례.
initialState: 처음에 state를 설정할 값. 모든 유형의 값일 수 있지만 함수일 경우 다르게 동작함. 이 인수는 초기 렌더링 이후에는 무시됨.initialState로 전달하면 initializer 함수로 취급됨. initializer 함수는 순수해야 하고 인수를 받지 않아야 하며 어떤 타입의 값이든 반환해야 함. React는 컴포넌트를 초기화할 때 initializer 함수를 호출하고 그 반환값을 초기 state로 저장함.두 개의 값을 가진 배열을 반환함:
initialState와 일치함.useState는 Hook이므로 컴포넌트의 최상위 레벨 또는 자체 Hook에서만 호출할 수 있음. 루프나 조건 내부에서는 호출할 수 없음. 필요하다면 새 컴포넌트를 추출해서 state를 그 안으로 옮겨야 함.
Strict Mode 에서는 실수로 발생한 불순물을 찾기 위해 React가 initializer 함수를 두 번 호출함. 이는 개발 전용 동작이며 프로덕션에는 영향을 미치지 않음. Initializer 함수가 순수하다면(그래야 함) 동작에 영향을 미치지 않음. 호출 중 하나의 결과는 무시됨.
useState가 반환하는 set 함수를 사용하면 state를 다른 값으로 업데이트하고 렌더링을 다시 트리거할 수 있음. 다음 state를 직접 전달하거나 이전 state로부터 다음 state를 계산하는 함수를 전달할 수 있음:
const [name, setName] = useState('Edward');
function handleClick() {
setName('Taylor');
setAge(a => a + 1);
// ...
nextState: 상태가 될 값. 모든 유형의 값이 될 수 있지만 함수일 경우 다르게 동작함.nextState로 전달하면 updater 함수로 취급됨. updater 함수는 순수해야 하고, pending state를 유일한 인수로 사용해야 하며, 다음 state를 반환해야 함. React는 updater 함수를 대기열에 넣고 컴포넌트를 다시 렌더링함. 다음 렌더링 중에 React는 대기열에 있는 모든 updater를 이전 state에 적용하여 다음 state를 계산함.반환값이 없음.
set 함수는 다음 렌더링에 대한 state 변수만 업데이트함. set 함수를 호출한 후 state 변수를 읽으면 호출 전 화면에 있던 이전 값을 계속 가져옴.
Object.is 비교에 의해 결정된 새 값이 현재 state와 동일하다면 React는 컴포넌트와 그 자식들을 다시 렌더링하는 것을 건너뜀(최적화). ?경우에 따라 React가 자식을 건너뛰기 전에 컴포넌트를 호출해야 할 수도 있지만, 코드에 영향을 미치지는 않음.?
React는 상태 업데이트를 일괄 처리함. 모든 이벤트 핸들러가 실행되고 set 함수를 호출한 후에 화면을 업데이트함. 이렇게 하면 단일 이벤트 중에 여러 번 다시 렌더링하는 것을 방지할 수 있음. 드물지만 DOM에 접근하기 위해 React가 화면을 더 일찍 업데이트하도록 강제해야 하는 경우, flushSync를 사용할 수 있음.
렌더링 도중 set 함수를 호출하는 것은 현재 렌더링 중인 컴포넌트 내에서만 허용됨. React는 해당 출력을 버리고 즉시 새로운 state로 다시 렌더링을 시도함. 이 패턴은 거의 필요하지 않지만 이전 렌더링의 정보를 저장하는 데 사용할 수 있음.
Strict Modde에서 React는 실수로 발생한 불순물을 찾기 위해 업데이터 함수를 두 번 호출함. 이는 개발 전용 동작이며 프로덕션에는 영향을 미치지 않음. 만약 업데이터 함수가 순수하다면(그래야 함), 이것은 동작에 영향을 미치지 않음. 호출 중 하나의 결과는 무시됨.
컴포넌트의 최상위 레벨에서 useState를 호출하여 하나 이상의 state 변수를 선언.
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(42);
const [name, setName] = useState('Taylor');
// ...
Array destructuring을 사용하여 [something, setSomething]처럼 state 변수의 이름을 지정하는 것이 관례.
useState는 두 개의 항목이 있는 배열을 반환함:
current state. 처음에는 제공한 initial state로 설정됨.set 함수.화면의 내용을 업데이트하려면 다음 state로 set 함수를 호출:
function handleClick() {
setName('Robin');
}
React는 다음 state를 저장하고 새로운 값으로 컴포넌트를 다시 렌더링한 후 UI를 업데이트함.
Pitfall
set함수를 호출해도 이미 실행 중인 코드의 현재 state는 변경되지 않음:function handleClick() { setName('Robin'); console.log(name); // Still "Taylor"! }다음 렌더링부터
useState가 반환할 값에만 영향을 줌.
age가 42라고 가정하고, 이 핸들러는 setAge(age + 1)를 세 번 호출함:
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}
그러나, 한 번의 클릭 이후에, age는 45가 아니라 43이 됨! 이는 set 함수를 호출해도 이미 실행 중인 코드에서 age state 변수가 업데이트되지 않기 때문. 따라서 각 setAge(age + 1) 호출은 setAge(43)이 됨.
이 문제를 해결하려면 다음 state 대신 update 함수를 setAge에 전달할 수 있음:
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
여기서 a => a + 1은 updater 함수임. 이 함수는 보류 중인 state를 가져와서 다음 state를 계산함.
React는 updater 함수를 대기열에 넣음. 그리고 다음 렌더링 중에 같은 순서로 호출함:
1.a => a + 1은 42를 보류 중인 state로 받고 다음 state로 43을 반환.
2.a => a + 1은 43를 보류 중인 state로 받고 다음 state로 44을 반환.
3.a => a + 1은 44를 보류 중인 state로 받고 다음 state로 45을 반환.
대기 중인 다른 업데이트가 없으므로 React는 결국 45를 현재 state로 저장함.
일반적으로 보류 중인 state 인수의 이름을 state 변수 이름의 첫 글자로 지정(예: age의 a). 하지만 더 명확하다고 생각되는 prevAge 또는 다른 이름으로 부를 수도 있음.
React는 개발 단계에서 업데이트가 순수한지 확인하기 위해 updater를 두 번 호출할 수 있음.
객체와 배열을 state에 넣을 수 있음. React에서 state는 읽기 전용으로 간주되므로 기존 객체를 변경하지 말고 대체해야함. 예를 들어 state에 form 객체가 있는 경우 변경해서는 안됨:
// 🚩 Don't mutate an object in state like this:
form.firstName = 'Taylor';
대신 새 객체를 생성하여 전체 객체를 교체해야함:
// ✅ Replace state with a new object
setForm({
...form,
firstName: 'Taylor'
});
참고: updating objects in state, updating arrays in state
React는 초기 상태를 한 번 저장하면 다음 렌더링에서 이를 무시함.
unction TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
// ...
createInitialTodos()의 결과는 초기 렌더링에만 사용되지만 여전히 모든 렌더링에서 이 함수를 호출함. 이는 큰 배열을 생성하거나 값비싼 계산을 수행하는 경우 낭비가 될 수 있음.
이 문제를 해결하기 위해 이 함수를 useState에 initializer 함수로 전달할 수 있음:
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
// ...
함수를 호출한 결과인 createInitialTodos()가 아니라 함수 자체인 createInitialTodos를 전달하고 있음에 주목할 것! 함수를 useState에 전달하면 React는 초기화 중에만 함수를 호출함.
React는 initializer가 순수한지 확인하기 위해 개발 단계에서 initializer를 두 번 호출할 수 있음.
목록을 렌더링할 때 key 속성을 자주 접하게 되지만 key 속성은 다른 용도로도 사용됨.
컴포넌트에 다른 key를 전달하여 컴포넌트의 상태를 재설정할 수 있음. 이 예제에서는 Reset 버튼이 version state 변수를 변경하고, 이를 Form에 key로 전달함. key가 변경되면 React는 Form 컴포넌트(및 그 모든 자식)를 처음부터 다시 생성하므로 상태가 초기화됨.
참고: preserving and resetting state
import { useState } from 'react';
export default function App() {
const [version, setVersion] = useState(0);
function handleReset() {
setVersion(version + 1);
}
return (
<>
<button onClick={handleReset}>Reset</button>
<Form key={version} />
</>
);
}
function Form() {
const [name, setName] = useState('Taylor');
return (
<>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<p>Hello, {name}.</p>
</>
);
}
일반적으로 이벤트 핸들러에서 state를 업데이트함. 하지만 드물게 렌더링에 대한 응답으로 state를 조정해야 하는 경우도 있음. 예를 들어 prop이 변경될 때 state 변수를 변경하고 싶을 수 있음.
대부분의 경우 위 작업은 필요하지 않음:
useMemo 훅을 사용하면 도움이 될 수 있음.key를 전달해야함.이 중 어느 것도 적용되지 않는 드문 경우, 컴포넌트가 렌더링되는 동안 set 함수를 호출하여 지금까지 렌더링된 값을 기반으로 state를 업데이트하는 패턴을 사용할 수 있음.
다음 예시에서 CountLabel 컴포넌트는 전달된 count prop을 표시함:
export default function CountLabel({ count }) {
return <h1>{count}</h1>
}
카운터가 마지막으로 변경된 이후 증가 또는 감소했는지 표시하고 싶다면, count prop은 이를 알려주지 않으므로 이전 값을 추적해야함. 이를 추적하기 위해 prevCount 상태 변수를 추가함.trend라는 또 다른 상태 변수를 추가하여 카운트의 증가 또는 감소 여부를 저장함. prevCount와 count를 비교하고 같지 않은 경우 prevCount와 trend를 모두 업데이트하면 현재 count prop과 마지막 렌더링 이후 count가 어떻게 변경되었는지를 모두 표시할 수 있음.
import { useState } from 'react';
export default function CountLabel({ count }) {
const [prevCount, setPrevCount] = useState(count);
const [trend, setTrend] = useState(null);
if (prevCount !== count) {
setPrevCount(count);
setTrend(count > prevCount ? 'increasing' : 'decreasing');
}
return (
<>
<h1>{count}</h1>
{trend && <p>The count is {trend}</p>}
</>
);
}
렌더링하는 동안 set 함수를 호출하는 경우, prevCount !== count와 같은 조건 안에 있어야 하며, 조건 안에 setPrevCount(count)와 같은 호출이 있어야 한다는 점에 유의할 것. 그렇지 않으면 컴포넌트가 충돌할 때까지 루프에서 다시 렌더링됨. 또한 이런 식으로 현재 렌더링 중인 컴포넌트의 state만 업데이트할 수 있음. 렌더링 중에 다른 컴포넌트의 set 함수를 호출하는 것은 오류! 마지막으로, set 호출은 여전히 변이 없이 state를 업데이트해야하고, 순수 함수의 다른 규칙들도 지켜야 함.
이 패턴은 이해하기 어려울 수 있으며 일반적으로는 피하는 것이 가장 좋음. 하지만 Effect에서 state를 업데이트하는 것보다는 나음. 렌더링 도중에 set 함수를 호출하면 React는 컴포넌트가 반환문으로 종료된 직후, 그리고 자식 컴포넌트를 렌더링하기 전에 해당 컴포넌트를 다시 렌더링함. 이렇게 하면 자식 컴포넌트를 두 번 렌더링할 필요가 없음. 나머지 컴포넌트 함수는 계속 실행되고 결과는 버려짐. 조건이 모든 Hook 호출보다 아래에 있으면 early return;을 추가하여 렌더링을 더 일찍 다시 시작할 수 있음.
set 함수를 호출해도 실행 중인 코드의 state는 변경되지 않음:
function handleClick() {
console.log(count); // 0
setCount(count + 1); // Request a re-render with 1
console.log(count); // Still 0!
setTimeout(() => {
console.log(count); // Also 0!
}, 5000);
}
이는 state가 스냅샷처럼 작동하기 때문. State를 업데이트하면 새 state 값으로 다른 렌더링을 요청하지만, 이미 실행 중인 이벤트 핸들러의 count JavaScript 변수에는 영향을 미치지 않음.
다음 state를 사용해야 하는 경우 변수에 저장한 후 set 함수에 전달하면 됨:
const nextCount = count + 1;
setCount(nextCount);
console.log(count); // 0
console.log(nextCount); // 1
Object.is 비교에 의해 결정된 대로 다음 state가 이전 state와 같으면 React는 업데이트를 무시함. 이는 보통 state의 객체나 배열을 직접 변경할 때 발생함:
obj.x = 10; // 🚩 Wrong: mutating existing object
setObj(obj); // 🚩 Doesn't do anything
기존 obj를 수정한 후 다시 setObj로 전달했기 때문에 React가 업데이트를 무시한 것. 이 문제를 해결하려면 항상 state 객체와 배열을 '수정'하는 대신 '교체'해야함:
// ✅ Correct: creating a new object
setObj({
...obj,
x: 10
});
Too many re-renders. React limits the number of renders to prevent an infinite loop. 와 같은 오류가 표시될 수 있음. React는 무한 루프를 방지하기 위해 렌더링 횟수를 제한함. 일반적으로 이것은 렌더링 중에 무조건적으로 state를 설정한다는 것을 의미하므로 컴포넌트는 렌더링, state 설정(렌더링 발생), 렌더링, state 설정(렌더링 발생)... 의 루프에 들어감. 이는 이벤트 핸들러를 지정할 때 실수로 인해 발생하는 경우가 많음:
// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>
// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>
// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>
이 오류의 원인을 찾을 수 없는 경우, 콘솔에서 오류 옆의 화살표를 클릭해서 JavaScript 스택을 살펴보고 오류의 원인이 되는 특정 set 함수 호출을 찾아볼 것.
Strict Mode에서 React는 일부 함수를 한 번이 아닌 두 번 호출함:
function TodoList() {
// This component function will run twice for every render.
const [todos, setTodos] = useState(() => {
// This initializer function will run twice during initialization.
return createTodos();
});
function handleClick() {
setTodos(prevTodos => {
// This updater function will run twice for every click.
return [...prevTodos, createTodo()];
});
}
// ...
이는 예상되는 현상이며 코드를 손상시키지 않아야함.
이 개발 환경 전용 동작은 컴포넌트를 순수하게 유지하는 데 도움이 됨. React는 호출 중 하나의 결과를 사용하고 다른 호출의 결과는 무시함. 컴포넌트, initializer, updater 함수가 순수하다면 로직에 영향을 미치지 않을 것! 그러나 의도치 않게 비순수한 경우, 실수를 알아차리는 데 도움이 됨.
예를 들어, 이 비순수한 updater 함수는 상태의 배열을 변경함:
setTodos(prevTodos => {
// 🚩 Mistake: mutating state
prevTodos.push(createTodo());
});
React는 updater 함수를 두 번 호출하기 때문에 할 일이 두 번 추가된 것을 볼 수 있으므로 실수가 있음을 알 수 있음. 이 예제에서는 배열을 '수정'하는 대신 '교체'하여 실수를 수정할 수 있음:
setTodos(prevTodos => {
// ✅ Correct: replacing with new state
return [...prevTodos, createTodo()];
});
이제 이 updater 함수는 순수 함수이므로 한 번 더 호출해도 동작에 차이가 없음. 그렇기 때문에 React가 두 번 호출하면 실수를 찾는 데 도움이 됨. 컴포넌트, initializer, updater 함수만 순수해야함. 이벤트 핸들러는 순수할 필요가 없으므로 React는 이벤트 핸들러를 두 번 호출하지 않음.
참고: 컴포넌트를 순수하게 유지하기
다음과 같은 방법으로는 state에 함수를 넣을 수 없음:
const [fn, setFn] = useState(someFunction);
function handleClick() {
setFn(someOtherFunction);
}
함수를 전달하고 있기 때문에 React는 someFunction이 initializer 함수이고, someOtherFunction이 updater 함수라고 가정하여 함수를 호출하고 결과를 저장하려고 시도함. 실제로 함수를 저장하려면 두 경우 모두 앞에 () =>를 넣어야함. 그러면 React는 사용자가 전달한 함수를 저장함.
const [fn, setFn] = useState(() => someFunction);
function handleClick() {
setFn(() => someOtherFunction);
}