state : 변경될 수 있는 데이터를 처리할 때 효율적으로 사용
setState()에 대해서 알아야 할 세 가지가 있습니다.
첫째, 직접 State를 수정하지 마세요
this.state.comment = 'Hello';
예를 들어, 이 코드는 Component를 다시 렌더링하지 않습니다.
→ 대신에 setState()를 사용합니다.
this.state를 지정할 수 있는 유일한 공간은 바로 constructor입니다.
모든 상태관리 변수, 객체를 직접 수정하면 안되며, 함수를 통해서 수정해야 한다.
(만약 변경 대상이 Array, Object이면), 객체는 해당 주소를 참조하므로 Mutate를 사용하지 않고 완전히 새로운 object나 array를 만들어주고, 함수를 통해서 수정되어야 한다.
둘째, State 업데이트는 비동기적일 수도 있습니다.
React는 성능을 위해 여러 setState() 호출을 단일 업데이트로 한꺼번에 처리할 수 있습니다.
this.props와 this.state가 비동기적으로 업데이트될 수 있기 때문에 다음 state를 계산할 때 해당 값에 의존해서는 안 됩니다.
→ setState()함수가 비동기적으로 동작하여 state에 업데이트는 늦게 처리될 수 있다.
→ React의 setState()가 비동기로 작동하는 이유는 렌더링 횟수를 줄여 더 빠르게 동작하게 하기 위함인 것을 알 수 있다.
→ merge 단계가 어떻게 이루어지는지는 Object.assign()
으로 객체를 합칠 때는 생각하면 이해할 수 있다. state도 결국 객체이기 때문에, 같은 키값을 가진 경우라면 가장 마지막 실행값으로 덮어씌워진다.
예를 들어, 다음 코드는 카운터 업데이트에 실패할 수 있습니다.
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
이를 수정하기 위해 객체보다는 함수를 인자로 사용하는 다른 형태의 setState()를 사용합니다.
그 함수는 이전 state를 첫 번째 인자로 받아들일 것이고, 업데이트가 적용된 시점의 props를 두 번째 인자로 받아들일 것입니다. ★★★
// Correct (Arrow function)
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
// Correct (Default function)
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
// *일반적인 함수에서도 정상적으로 작동합니다.*
셋째, State 업데이트는 병합됩니다.
setState()를 호출할 때 React는 제공한 객체를 현재 state로 병합합니다.
예를 들어, state는 다양한 독립적인 변수를 포함할 수 있습니다.
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
별도의 setState() 호출로 이러한 변수를 독립적으로 업데이트 할 수 있습니다.
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
병합은 얕게 이루어지기 때문에 this.setState({comments})는 this.state.props에 영향을 주진 않지만 this.state.comments 는 완전히 대체됩니다.
⚠️ React setState() 동작 내부 처리 방식
React 내부적으로 setState를 비동기로 처리한다.
하나의 어플리케이션, 하나의 페이지나 컴포넌트에도 수많은 상태값이 있을텐데, 상태가 각각 바뀔 때마다 화면을 리랜더링 해야한다면 문제가 생길 것이다.
*그래서 리액트는 이런 문제를 효율적으로 해결하기 위해 setState를 연속 호출하면 setState를 모두 **취합(batch)*한 후에 한 번에 렌더링하도록 한다. 그러면 100번의 상태값 변화를 한 번의 렌더링으로 최신의 상태를 갱신할 수 있다.
export default class App extends Component {
state = { count: 0 };
*// 1. 객체 state의 값 전달*
handleClick = () => {
this.setState({ count: this.state.count + 1 });
console.log("call-1: " + this.state.count); // 1
this.setState({ count: this.state.count + 1 });
console.log("call-2: " + this.state.count); // 1
this.setState({ count: this.state.count + 1 });
console.log("call-3: " + this.state.count); // 1
};
*// setState()는 비동기적으로 동작하기 때문에 참조값 this.state.count는
// 이전 값으로 반환된다.
// state에 업데이트된 count는 1 만큼씩 만 증가한다.*
*// 2. 함수 인자의 값 전달*
handleClick = () => {
this.setState((prevState) => ({ count: prevState.count + 1 }));
console.log(this.state.count); // 1
this.setState((prevState) => ({ count: prevState.count + 1 }));
console.log(this.state.count); // 2
this.setState((prevState) => ({ count: prevState.count + 1 }));
console.log(this.state.count); // 3
};
*// setState()는 비동기적으로 동작하기 때문에 참조값 this.state.count는*
*// 이전 값으로 반환된다.*
*// 하지만 prevState 매게변수로 변경된 state.count 의 값을 불러와 사용할 수 있다.*
*// (this.state.count 가 변경된 게 아니라 prevState가 변경된 값이다.)*
*// state에 업데이트된 count는 3 만큼씩 증가한다.*
render() {
console.log("render!");
return (
<>
<div>현재값 {this.state.count}</div>
<button onClick={this.handleClick}>count+</button>
</>
);
}
}
// -------------------------
// (추가)
// setState 함수가 호출되고 난 후 동작을 정의하기
handleClick = () => {
this.setState((prevState) => ({ count: prevState.count + 1 }),
() => {
console.log('방금 setState가 호출되었습니다.');
});
};
부모 Component나 자식 Component 모두 특정 Component가 유상태인지 또는 무상태인지 알 수 없고, 그들이 함수나 클래스로 정의되었는지에 대해서 관심을 가질 필요가 없습니다.
이 때문에 state는 종종 로컬 또는 캡슐화라고 불립니다. state가 소유하고 설정한 Component 이외에는 어떠한 Component에도 접근할 수 없습니다.
Component는 자신의 state를 자식 Component에 props로 전달할 수 있습니다.
<FormattedDate date={this.state.date} />
FormattedDate Component는 date를 자신의 props로 받을 것이고 이것이 어떤 Component의 state로부터 왔는지 수동으로 입력한 것인지 알지 못합니다.