[React] useState
리액트에서 컴포넌트는 함수다.
jsx 코드에서는 컴포넌트들을 HTML 요소처럼 사용하는데, 컴포넌트를 사용해서 리액트에 컴포넌트 함수를 알려주는 것이다.
const Expenses = ({ expenses }) => {
return (
<div>
<Card className='expenses'>
<ExpenseItem
title={expenses[0].title}
amount={expenses[0].amount}
date={expenses[0].date}
/>
</Card>
</div>
);
};
export default Expenses;
리액트는 jsx코드를 평가할 때마다 컴포넌트 함수를 호출한다. 그러면 컴포넌트 함수들이 평가할 jsx 코드가 없을 때까지, 즉 호출할 컴포넌트 코드가 남지 않을 때까지 jsx 코드를 리턴하면서 모든 컴포넌트 함수를 실행한다.
그 다음 전체 결과를 다시 평가하고 그 결과를 DOM 명령으로 전환한 것이 화면에 렌더링 되는 것이다.
리액트는 앱이 처음 렌더링될 때 이 모든 과정을 한 번 거친다. 따라서 화면에 표시되는 내용을 업데이트하려면 특정 컴포넌트가 재평가(다시 실행)되어야 하는데, 이때 필요한 개념이 상태이다.
const ExpenseItem = (props) => {
let title = props.title;
const clickHandler = () => {
title = 'Updated!'
console.log(title);
};
return (
<Card className='expense-item'>
<ExpenseDate date={props.date} />
<div className='expense-item__description'>
<h2>{title}</h2>
<div className='expense-item__price'>${props.amount}</div>
</div>
<button onClick={clickHandler}>Change Title</button>
</Card>
);
};
export default ExpenseItem;
여기서 버튼을 클릭하면 사용자 화면에 보여지는 title이 바뀌어야 할 것 같은데 바뀌지 않는 이유가 바로 컴포넌트가 한 번 실행되기 때문이다.
컴포넌트를 다시 실행한다해도 title에 props.title이 저장되어 화면에는 업데이트한 내용이 보이지 않을 것이다. 따라서 일반적인 변수로는 안되는 것이고, 리액트의 상태라는 개념이 필요한 것이다!
바뀔 수 있는 데이터가 있고, 데이터 변화가 UI에 반영되어야 한다면 상태가 필요하다. JS 일반 변수로는 이를 반영할 수 없다!
useState
리액트 라이브러리가 제공하는 useState 함수로 값을 상태로 정의한다. -> 해당 컴포넌트 인스턴스에 대한 상태를 등록한다.
useState는 리액트 hook이라고 부르는데, 이러한 hook들은 모두 컴포넌트 함수 안에서 호출되어야 한다.
const [title, setTitle] = useState(props.title);
useState에 인수로 초기값을 넣어준다. 이 함수가 호출되면 배열을 리턴하는데, 첫 번째 값은 상태 값, 두 번째 값은 상태를 업데이트하는 함수이다.
업데이트 함수는 메모리에서 리액트에 의해 관리되는데, 이 상태 업데이트 함수를 호출할 때 인수로 넣은 새 값으로 상태 값을 변경하고, 상태 업데이트 함수를 호출한(useState로 상태를 초기화한) 컴포넌트 함수를 재실행한다.
리액트는 해당 컴포넌트 함수를 다시 실행하고 jsx코드도 다시 평가한다.
🧐 컴포넌트가 재실행되면 useState를 호출한 코드를 다시 실행할텐데 그럼 초기값을 계속 덮어쓰는 거 아닌가?
> 리액트는 주어진 컴포넌트 인스턴스에서 처음 useState를 호출하는 때를 추적한다. 그래서 컴포넌트가 상태 변화에 의해 재실행되더라도 리액트는 상태를 다시 초기화하지 않는다.
해당 상태가 이전에 초기화된 적 있다는 사실을 깨닫고 상태 업데이트를 기반으로 하는 최신 상태를 확인해서 돌려준다.
따라서 초기값은 최초로 useState가 실행되는 때에만 고려되고, 최초의 초기화 이후에는 갱신만 된다.
상태 업데이트
상태 업데이트 함수를 호출하면 값을 즉시 바꾸지 않고 상태 업데이트를 예약한다. 상태 변화가 처리되면 리액트가 컴포넌트를 재평가하게 되면서 업데이트된 값을 볼 수 있는 것이다.
이전 상태에 의존해 상태를 업데이트하는 경우
const [userInput, setUserInput] = useState({
enteredTitle: '',
enteredAmount: '',
enteredDate: '',
});
const titleChangeHandler = (event) => {
setUserInput({
...userInput,
enteredTitle: event.target.value,
});
};
위 코드는 title만 업데이트할 때 다른 값들을 잃지 않기 위해 기존의 값을 스프레드 문법(...)을 사용해 복사 한뒤 새 값으로 이전 값을 덮어쓴 것이다.
상태를 업데이트 할 때 이전 상태에 의존하는 경우, 위 코드처럼 하면 안되고 상태 업데이트 함수에 함수를 넣는 양식을 사용해야 한다.
setUserInput((prevState)=>{
return { ...prevState, enteredTitle: event.target.value };
});
setUserInput에 넣은 함수는 리액트가 자동으로 실행시키고, 이전 상태 스냅샷을 받는다.(prevState) 이 이전 상태 스냅샷을 얻고 새 상태 스냅샷을 리턴하면 된다.
두 방식 모두 작동은 하지만, 리액트는 상태 업데이트를 예약(업데이트 함수 호출)시 즉시 처리하지 않는다. 따라서 다수의 상태 업데이트를 동시에 예약할 경우 잘못된 상태 스냅샷에 의존할 수도 있기 때문에 아래 코드의 방식을 선택해야 한다.
리액트는 상태 업데이트 함수 내부 함수에서 제공하는 상태 스냅샷이 항상 최신 상태 스냅샷이 되도록 보장해 준다.
물론 상태 업데이트가 매우 빠르게 처리가 되기 때문에 많은 경우에 크게 문제 되지 않지만 이론상으로 이 예약 작업은 지연될 수 있기 때문에 이전 상태에 의존해 상태를 업데이트 할 경우에는 안전하게 하기 위해 함수 양식을 사용해야 한다!