지난 시간에 state에 대해서 다루면서 간단한 state를 다뤄봤다.
이번 시간에는 사용자가 입력하는 여러 값들에 대해서 state를 업데이트하는 방법에 대해서 다뤄볼까한다.
How to update State?
state를 사용한다고 선언할 때에 useState라는 hook을 사용한다는 것은 앞시간에 다뤘기에 알 것이다.
그렇다면 여러 개의 state는 어떻게 사용할까?
다음 예제들은 사용자로부터 title, amount, date를 받는 코드이다.
여러 예제들을 보고 어떤 코드가 나은지 직접 판단하기 바란다.
예제 1
const [enteredTitle, setEnteredTitle] = useState('') ;
const [enteredAmount, setEnteredAmount] = useState('') ;
const [enteredDate, setEnteredDate] = useState('') ;
const titleChangeHandler = (event) => {
setEnteredTitle(event.target.value) ;
};
const amountChangeHandler = (event) => {
setEnteredAmount(event.target.value) ;
};
const dateChangeHandler = (event) => {
setEnteredDate(event.target.value) ;
};
return (
<form>
<div className="new-expense__controls">
<div className="new-expense__control">
<label>Title</label>
<input type="text" onChange={titleChangeHandler} />
</div>
<div className="new-expense__control">
<label>Amount</label>
<input
type="number"
min="0.01"
step="0.01"
onChange={amountChangeHandler}
/>
</div>
<div className="new-expense__control">
<label>Date</label>
<input
type="date"
min="2019-01-01"
max="2023-12-31"
onChange={dateChangeHandler}
/>
</div>
</div>
<div className="new-expense__actions">
<button type="submit">Add Expense</button>
</div>
</form>
);
해당 예제는 기존 하나만 존재했던 state를 필요한 만큼(3개) 늘려서 사용한 경우이다.
예제 2
예제 1의 경우, 지극히 정상적이지만 다른 대안도 존재합니다.
해당 코드는 같은 개념이 3번 반복된 코드입니다.
따라서, 3번의 state 대신 1번으로 처리할 수 있습니다.
const [userInput, setUserInput] = useState({
enteredTitle: "",
enteredAmount: "",
enteredDate: "",
});
const titleChangeHandler = (event) => {
setUserInput({
...userInput,
enteredTitle: event.target.value,
});
};
const amountChangeHandler = (event) => {
setUserInput({
...userInput,
enteredAmount: event.target.value,
});
};
const dateChangeHandler = (event) => {
setUserInput({
...userInput,
enteredDate: event.target.value,
});
};
return (
<form>
<div className="new-expense__controls">
<div className="new-expense__control">
<label>Title</label>
<input type="text" onChange={titleChangeHandler} />
</div>
<div className="new-expense__control">
<label>Amount</label>
<input
type="number"
min="0.01"
step="0.01"
onChange={amountChangeHandler}
/>
</div>
<div className="new-expense__control">
<label>Date</label>
<input
type="date"
min="2019-01-01"
max="2023-12-31"
onChange={dateChangeHandler}
/>
</div>
</div>
<div className="new-expense__actions">
<button type="submit">Add Expense</button>
</div>
</form>
);
useState
를 호출할 때 객체 형태로 전달하면 됩니다.
여기서 중요한 것은 문자열이나 숫자같은 변수가 아니라 객체라는 것이다.
문제는 이런 방법으로 state를 제작한다면 state를 업데이트할 때마다 1개의 property만이 아니라 3개의 properties를 모두 업데이트해야한다는 것이다.
만약 변경된 property만 업데이트한다면, 다른 properties는 버려지게 된다.
왜냐하면 state를 업데이트할 때, React는 이 정보들을 이전의 state와 병합하지 않기 때문이다.
단순히 예전 state를 새 것으로 대체한다.
그래서 만약 새로운 state가 {key : value}
를 가진 한 쌍의 객체라면 이전의 state는 대체되고 나머지 properties를 잃어버린다.
그래서 state에 접근할 때에는 다른 모든 value들이 사라지지 않게하기 위해 업데이트되지 않는 다른 값들을 수동으로 복사해야한다.
여기에 한가지 방법은 spread operator를 사용하는 것이다.
그래서 위의 예제 2와 같이 먼저 복사하고, 입력된 state를 override
한다.
예제 3
예제 2와 같이 여러 state들을 하나로 묶어서 사용할 수도 있다.
하지만 일반적으로 접근 방식을 바꿀 수 있다.
예제 2에서의 접근 방식에 좋지 못한 점이 한가지 있다면 값을 복사하기 위해 이전 state를 고려할 때 어떻게 state를 업데이트하는 가이다.
예제 2에서는 3개의 state를 하나로 접근하는 방식을 사용하기에 이전 state에 의존할 수 밖에 없다.
그렇기에, 다른 properties를 복사해서 이전 state의 snapshot에 의존하고 그 다음 새로운 값으로 override하게 되었다.
예를 들어, 하나씩 증가하는 카운터를 관리한다고 했을 때, state를 업데이트할 때마다 이전 state에 의존해야한다.
이런 경우에는 함수를 호출하는 대신 호출해서 그 함수에 바로 전달해야한다.
아래의 예제를 보자 : )
const [userInput, setUserInput] = useState({
enteredTitle: "",
enteredAmount: "",
enteredDate: "",
});
const titleChangeHandler = (event) => {
setUserInput((prevState) => {
return { ...prevState, enteredTitle: event.target.value };
});
};
const amountChangeHandler = (event) => {
setUserInput((prevState) => {
return { ...prevState, enteredAmount: event.target.value };
});
};
const dateChangeHandler = (event) => {
setUserInput((prevState) => {
return { ...prevState, enteredDate: event.target.value };
});
};
return (
<form>
<div className="new-expense__controls">
<div className="new-expense__control">
<label>Title</label>
<input type="text" onChange={titleChangeHandler} />
</div>
<div className="new-expense__control">
<label>Amount</label>
<input
type="number"
min="0.01"
step="0.01"
onChange={amountChangeHandler}
/>
</div>
<div className="new-expense__control">
<label>Date</label>
<input
type="date"
min="2019-01-01"
max="2023-12-31"
onChange={dateChangeHandler}
/>
</div>
</div>
<div className="new-expense__actions">
<button type="submit">Add Expense</button>
</div>
</form>
);
호출된 setUsetInput
함수에서 바로 이전 state의 snapshot을 받은 것이다.
물론 예제 2와 예제 3 모두 괜찮은 코드이다.
하지만 React는 state 업데이트 스케줄을 가지고 있어 호출 즉시 실행되지 않는다
그래서 이론적으로 동시에 수많은 state 업데이트를 계획한다면 오래되었거나 잘못된 state snapshot에 의존하게 되는 경우가 발생할 수 있다.
그렇기에 해당 방법을 사용한다면, React는 이 안에 있는 함수에서 이 state snapshot이 가장 최신 상태의 snapshot이라는 것과 항상 계획된 state 업데이트를 염두에 두고 있다는 것을 보장한다.
따라서 해당 방법으로 코딩하는 것은 항상 최신 상태의 snapshot에서 작업하도록 하는 좀 더 안전한 방법이다.
만약 state 업데이트가 이전 state에 의존하는 경우, 예제 3의 폼을 기억하길 바란다.
앞으로 수많은 과정과 프로젝트에서 반복해서 보게될 핵심 개념일 것이다.