
useState : 컴포넌트에 state 변수를 추가할 수 있는 React Hook
import { useState } from 'react'
const [state, setState] = useState(initialState)
useState는 정확히 두 개의 값을 가진 배열을 반환한다.
1. 현재 state, 첫 번째 렌더링 중에는 전달한 initialState와 일치한다.
2. state를 다른 값으로 업데이트하고 리렌더링을 촉발할 수 있는 set 함수
useState가 반환하는 set 함수를 사용하면 state를 다른 값으로 업데이트하고 리렌더링을 촉발할 수 있다. 여기에는 다음 state를 직접 전달하거나, 이전 state로부터 계산한 함수를 전달할 수도 있다.
state가 될 값이다. 값은 모든 데이터 타입이 허용되지만, 함수에 대해서는 특별한 동작이 있다.
함수를 nextState로 전달하면 업데이터 함수로 취급한다. 이 함수는 순수해야 하고, 대기 중인 state를 유일한 인수로 사용해야 하며, 다음 state를 반환해야 한다.
set 함수는 반환값이 없다.
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(42);
const [name, setName] = useState('Taylor');
// ...
useState는 정확히 두 개의 항목이 있는 배열을 반환한다.
화면에 내용을 업데이트하려면 다음 state로 set 함수를 호출한다.
function handleClick() {
setName('seunghee');
}
⚠️ 주의사항
React는 다음 state를 저장하고 새로운 값으로 컴포넌트를 다시 렌더링한 후 UI를 업데이트한다.
set 함수를 호출해도 이미 실행 중인 코드의 현재 state는 변경되지 않는다❗️function handleClick() { setName('seunghee'); console.log(name); // 아직 "Taylor"입니다! }
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)이 된다.
이 문제를 해결하려면 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은 업데이터 함수이다.
이 함수는 대기 중인 state를 가져와서 다음 state를 계산한다.
React는 업데이터 함수를 큐에 넣는다. 그러면 다음 렌더링 중에 동일한 순서로 호출한다.
- a => a + 1은 대기 중인 state로 42를 받고 다음 state로 43을 반환
- a => a + 1은 대기 중인 state로 43을 받고 다음 state로 44를 반환
- a => a + 1은 대기 중인 state로 44를 받고 다음 state로 45를 반환
대기 중인 다른 업데이트가 없으므로, React는 결국 45를 현재 state로 저장한다.
규칙상 대기 중인 state 인수의 이름을 age의 a와 같이 state 변수 이름을 첫 글자로 지정하는 것이 일반적이다. 하지만 더 명확하다고 생각하는 다른 이름으로 지정해도 된다.
import { useState } from 'react';
export default function Counter() {
const [age, setAge] = useState(42);
function increment() {
setAge(a => a + 1);
}
return (
<>
<h1>Your age: {age}</h1>
<button onClick={() => {
increment();
increment();
increment();
}}>+3</button>
</>
);
}
업데이터 함수를 전달하므로 +3 버튼이 작동한다.
import { useState } from 'react';
export default function Counter() {
const [age, setAge] = useState(42);
function increment() {
setAge(age + 1);
}
return (
<>
<h1>Your age: {age}</h1>
<button onClick={() => {
increment();
increment();
increment();
}}>+3</button>
</>
);
}
업데이터 함수를 전달하지 않으므로 +3 버튼이 의도한 대로 작동하지 않고, +1 된다.
state에는 객체와 배열도 넣을 수 있다. React에서는 state는 읽기 전용으로 간주되므로 기존 객체를 변경하지 않고, 교체해야 한다. 예를 들어, state에 form 객체가 있는 경우 변경하면 안된다.
// 🚩 state 안에 있는 객체를 다음과 같이 변경하지 마세요.
form.firstName = 'Taylor';
// ✅ 새로운 객체로 state를 교체해야 한다.
setForm({
...form,
firstName: 'Taylor'
});
이 예시에서 form state 변수는 객체를 받는다.
import { useState } from 'react';
export default function Form() {
const [form, setForm] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: 'bhepworth@sculpture.com',
});
return (
<>
<label>
First name:
<input
value={form.firstName}
onChange={e => {
setForm({
...form,
firstName: e.target.value
});
}}
/>
</label>
<label>
Last name:
<input
value={form.lastName}
onChange={e => {
setForm({
...form,
lastName: e.target.value
});
}}
/>
</label>
<label>
Email:
<input
value={form.email}
onChange={e => {
setForm({
...form,
email: e.target.value
});
}}
/>
</label>
<p>
{form.firstName}{' '}
{form.lastName}{' '}
({form.email})
</p>
</>
);
}
각 input에는 전체 form의 다음 state로 setForm을 호출하는 change 핸들러가 있다. 전개 구문인 { ...form }은 state 객체를 변경하지 않고 교체한다.
React는 초기 state를 한 번 저장하고 다음 렌더링부터는 이를 무시한다.
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
// ...
createInitialTodos()의 결과는 초기 렌더링에만 사용되지만, 여전히 모든 렌더링에서 이 함수를 호출한다. 이는 큰 배열을 생성하거나 값비싼 계산을 수행하는 경우 낭비일 수 있다.
이 문제를 해결하려면, useState에 초기화 함수로 전달하면 된다.
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
// ...
함수를 호출한 결과인 createInitialTodos()가 아니라 함수 자체인 createInitialTodos를 전달하고 있다. 함수를 useState에 전달하면 React는 초기화 중에만 함수를 호출한다.
const [todos, setTodos] = useState(createInitialTodos);
위 코드는 초기화 함수를 전달하므로, createInitialTodos 함수는 초기화 중에만 실행된다.
input에 타이핑할 때 같이 컴포넌트가 리렌더링할 때에는 실행되지 않는다.
const [todos, setTodos] = useState(createInitialTodos());
위 코드는 초기화 함수를 전달하지 않으므로, input을 타이핑할 때 같이 모든 렌더링에서 createInitialTodos 함수가 실행된다. 동작에 눈에 띄는 차이는 없지만 이 코드는 효율성이 떨어진다.
컴포넌트에 다른 key를 전달하여 컴포넌트의 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>
</>
);
}
Reset 버튼이 version state 변수를 변경하고, 이를 form에 key로 전달한다.
key가 변경되면 React는 Form 컴포넌트를 처음부터 다시 생성하므로 state가 초기화된다.