(React 공식문서의 main concepts 번역 글입니다.)
함께보면 이해가 쏙쏙
state는 component의 상태이다.
props가 component에게 주어지는 data라면
state는 이 props의 값이 변할 때
변하는 props의 data를 component가 스스로 업데이트하여
UI로 표시할 수 있게 한다.
lifecycle은 component가
만들어지고 렌더되고 사라지는 일련의 과정이다.
이 lifecycle과 state는 어떠한 연관이 있을까?
아래 계속 변하는 props를 가진 tick component 예제를 통해
lifecycle과 state의 관계 와
component가 스스로 업데이트하는 과정 에 대해 알아보자
우리는 props로 주어지는 data 값의 변화를
UI에 업데이트하는 방법을 1가지 배웠다.
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render( element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
그 방법은 아래의 과정을 거친다.
값이 변할때마다 위 과정을 모두 거치는 것은 비효율적이다.
우리는 component가 변하는 props값을 스스로 업데이트하길 원한다.
어떻게 가능할까?
props 값이 변하는 것을 UI에 업데이트하는 방법은 1가지가 더 있다.
아래 tick component를 통해 그 방법을 알아보자.
tick component는 Clock component를 extract하여
렌더링 함수와 react element을 분리할 수 있다.
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}
function tick() {
ReactDOM.render(
<Clock date={new Date()} />, document.getElementById('root')
);
}
setInterval(tick, 1000);
Clock component 즉, tag가 포함된 react element를 extract했다.
그 결과, UI요소인 tag들이 encapsulated 되어
어떤 tag를 포함하고 있는지 보이지 않게 되었다.
기존에 tick component에는 렌더링 함수만 남아있게 되었다.
하지만 아직 해결하지 못한 문제가 있다.
Clock component에게 props로 date 값이
고정적으로 주어지고 있으므로(하이라이팅된 부분)
매초마다 바뀌는 date 값을 표시하기 위해
setInterval로 tick을 계속 다시 실행해야하는 문제가 남아있다.
우리는 Clock component에게 date값을 props로 주지 않아도
Clock component가 스스로 업데이트 하길 원한다.
마치 아래 코드에서 하이라이팅된 부분처럼!
ReactDOM.render(
<Clock />, document.getElementById('root')
);
위 코드처럼 변하는 props를 반복해서 주지 않아도
Clock component가 스스로 업데이트 하기 위해서는
Clock component에게 state를 주어야한다.
state는 props와 비슷하지만 private하고 component에 의해 완전하게 controll 된다.
state를 주기 위해서는 Clock component가 function에서 class로 바뀌어야 한다.
아래와 같은 과정을 통해 function에서 class로 바꿀 수 있다.
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
위에 render() method는 업데이트 될때마다 실행된다.
하지만 같은 DOM node에서 Clock을 render한다면
Clock class는 한개의 instance를 만들고
그 instance가 계속 사용되어 진다.
그 결과 state, lifecycle method 등의
추가기능을 사용할 수 있다.
아래 과정을 통해 date라는 props를 state로 바꿀 수 있다.
this.props.date를 this.state.date로 바꾼다
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div>
);
}
}
class constructor를 선언하고 state에 초기값을 준다.
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()}; }
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
constructor에 props를 상속한다.
(class component는 항상 super(props)로
부모 class로부터 props를 상속 받아서 시작한다)
constructor(props) {
super(props); this.state = {date: new Date()};
}
기존 Clock component에 props로 주었던 date를 지운다.
ReactDOM.render(
<Clock />, document.getElementById('root')
);
나중에 바뀌는 시간을 표시하기 위한 코드를
Clock component에 직접 추가한다.
최종적인 결과는 아래와 같다.
class Clock extends React.Component {
constructor(props) { super(props); this.state = {date: new Date()}; }
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div>
);
}
}
ReactDOM.render(
<Clock />, document.getElementById('root')
);
이제 바뀌는 시간을 표시하기 위한 코드를 추가하자 그 결과, Clock component는 스스로 업데이트 할 수 있게 될 것이다.
Clock component가 DOM에 반복해서 render되는 순간을 캐치하기위해
타이머를 설정해야 한다.
component가 DOM에 렌더되는 순간을 mounting 라고 한다.
또한, 이 Clock component가 만든 DOM이 삭제될 때
이 타이머도 같이 삭제되어야 한다.(메모리 관리를 위해)
component가 DOM에서 삭제되는 순간을 unmounting 라고 한다.
lifecycle method를 활용하면
이 타이머가 울릴 때, 실행되어야 할 코드를 추가할 수 있다.
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
}
componentWillUnmount() {
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
componentDidMount와 componentWillUnmount라는
lifecycle method를 위와 같이(하이라이팅된 부분) 추가했다.
component가 return한 element가 DOM에 render된 후
실행되는 method이다.
아래 코드를 추가해주면
Clock component가 DOM에 render된 후
tick을 반복 실행하는 타이머가 설정된다.
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
이때, timerID는 바로 this.timerID로 저장할 수 있다.
data flow와 연관 있는 것들(props, state)이 아닌 다른 것들(timerID)은 얼마든지 this에 바로 저장할 수 있다.
component가 return한 element가 DOM에서 제거된 후
실행되는 method이다.
componentWillUnmount를 활용하면 설정했던 타이머를 제거할 수 있다.
(component가 제거되면 관련된 resource(timer 등)를
함께 제거해야 메모리 낭비가 없다.)
componentWillUnmount() {
clearInterval(this.timerID);
}
tick은 기존에 렌더링 함수를 분리한 것이었다.
하지만 state 값, 즉 바뀌는 시간을 수정하는 method가 되어야 한다.
tick() {
this.setState({
date: new Date()
});
}
setState라는 function을 통해 state값을 수정할 수 있다.
(setState에 대한 자세한 설명은 아래에서 추가로 하겠다)
우선 완성된 clock component의 코드를 보자
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
ReactDOM.render()가 아예 밖으로 빠졌다.
Clock component는 class로 선언되어
state와 lifecycle method가 추가될 수 있었다.
위 코드가 실행되는 과정은 아래와 같다.
<Clock />
를 call한다.반드시 setState()를 사용하자.
절대 state에 수정된 값을 직접 할당하지 마라
state에 값을 직접 할당할 수 있는 곳은
constructor안에서 초기값을 할당할 때 뿐이다.
리액트는 같은 object를 argument로 받은 setState를
여러번 call하면 한번만 실행한다.(performance를 위해서)
this.props와 this.state는 비동기적으로 업데이트 되기 때문에
다음 state값을 계산할 때 현재 props와 state값을 사용하면 안된다.
아래 코드는 state를 완벽하게 업데이트할 수 없다.
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
이 문제를 해결하기 위해서는 setState의 argument로
object대신 function을 주어야 한다.
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
이 function은 바뀌기전 state값과 업데이트 될 때 props값을 argument로 받는다.
setState()를 call하면 React는 setState()의 argument로 받은 object를 현재 state 값에 merge한다.
아래처럼, state값이 몇개의 key-value 페어로 이루어져 있다면
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
setState()를 여러번 call해서 state안에 각 key-value 페어를 수정할 수 있다.
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
이때 merge는 shallow copy이다.
따라서 this.setState({comment})
는
오직 comments만 수정한다.(posts는 수정하지 않는다.)
부모 component나 자식 component 모두
어떤 특정 component가 stateful한지 stateless한지,
혹은 function component인지 class component인지 알 수 없다.
이것이 바로
state는 local하다 혹은 encapsulated하다 라고 말하는 이유이다.
component는 다른 component의 state에 접근하거나 출처를 알 수 없다.
유일하게 접근하는 방법이 있다. component는 자신의 상태를 하위 component에게 props로 전달할 수 있다.
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
또한 사용자 정의 component에게도 props로 전달할 수 있다.
<FormattedDate date={this.state.date} />
FormattedDate component는 date를 props로 받는데
이 date에 부모 component의 state 값이 할당되어 있으므로
접근할 수 있다. 하지만 FormattedDate가 state의 출처를 알 수는 없다.
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
그저 props로 받아서 값에 접근할 수 있을 뿐이다.
이것을 Top-down data flow라고 부른다.
React는 아래와 같은 특징을 가진다.
props를 부모 component에서 자식 component로 흐르는 폭포라고 가정하면 state는 중간 중간 component들로부터 추가되는 물줄기라고 할 수 있다.
모든 component들이 독립적으로 구성되어져 있다.
아래 App component를 보자.
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
총 3개의 Clock이 있다.
각 Clock은 각자의 타이머가 설정되며 독립적으로 업데이트 된다.
React 앱에서 component가 stateful한지 혹은 stateless한지는 중요하지 않다. 왜냐하면 바뀔 수 있기 때문이다. 또한 Stateful한 component 내부에서 stateless한 component를 사용할 수 있으면 그 반대도 가능하다.