여기서는 React 컴포넌트의 state와 생명주기의 개념을 설명한다.
여기서 상세 컴포넌트 API를 볼 수 있다.
지난번에 똑딱이는 시계 예제를 보았다. Rendering Elements에서 UI를 업데이트하는 한가지 방법을 배웠다. 렌더링된 출력을 바꾸는 것을 ReactDOM.render()
라고 불렀다:
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);
여기서 Clock
컴포넌트를 재사용 가능하고 캡슐화 되도록 만드는 방법을 배웠다. Clock
컴포넌트는 자체 타이머를 설정하고 매초 자체적으로 갱신한다.
시계가 어떻게 보일지 캡슐화해보자.
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
이 타이머를 설정하고 매초 UI를 갱신한다는 사실은 Clock
의 구현 세부사항이어야 한다.
이상적으로는 이것을 한번만 작성하고 Clock
이 스스로 갱신하도록 해야한다.
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
이것을 구현하기 위해서, "state"
를 Clock
컴포넌트에 추가해야 한다.
State는 props와 비슷하지만 private하고 완전히 컴포넌트에 의해 컨트롤된다는 점이 다르다.
Clock
같은 함수형 컴포넌트를 클래스로 바꾸는 5가지 단계이다:
1. React.Component
를 extends하는 같은 이름의 ES6 class
를 생성한다.
2. render()
라고 부르는 비어있는 메서드를 추가한다.
3. 함수 내용을 render()
메서드로 옮긴다.
4 render()
안에 있는 props
를 this.props
로 대체한다.
5. 남아있는 빈 함수 선언을 삭제한다.
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
Clock
은 이제 함수가 아닌 클래스로 정의되었다.
render
메서드는 업데이트가 일어날 때마다 호출된다. 하지만 같은 DOM노드에 <Clock />
을 렌더링하면 Clock
클래스에서 오직 하나의 인스턴스만 사용된다. 이건 local state와 lifecycle 메서드 같은 부가적인 특징을 사용할 수 있게 해준다.
date
를 props에서 state로 세단계에 걸쳐 이동해보겠다:
1.render()
메서드에서 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>
);
}
}
this.state
를 할당하는 class 생성자를 추가한다: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>
);
}
}
어떻게 props
를 기본 생성자에 넘겨주는지 주목해보자:
constructor(props) {
super(props);
this.state = {date: new Date()};
}
클래스 컴포넌트는 항상 props
와 함께 기본 생성자를 호출해야 한다.
<Clock />
element로부터 date
prop을 지운다:ReactDOM.render(
<Clock />,
document.getElementById('root')
);
나중에 타이머 코드를 다시 본래 컴포넌트로 돌려놓을 것이다.
결과는 다음과 같을 것이다:
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
이 매초 자신의 타이머를 설정하고 스스로를 갱신하도록 만들어보자.
여러 컴포넌트들로 이루어진 애플리케이션들에서, 컴포넌트가 없어질 때 컴포넌트가 차지하고 있던 자원을 해제하는 것이 중요하다.
Clock
이 DOM에 처음 렌더링될 때마다 타이머를 설정하고자 한다. 이것을 React에 "mounting"한다고 한다.
또한 Clock
에 의해 생성된 DOM이 제거될 때마다 타이머를 클리어하고자 한다. 이를 React에 "unmounting"한다고 한다.
컴포넌트를 mount하고 unmount할 때 코드를 실행하기 위해 컴포넌트 클래스에 특별한 메서드들을 선언할 수 있다:
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>
);
}
}
이러한 메서드들을 "lifecycle methods"라고 부른다.
컴포넌트 output이 DOM에 렌더링된 후에 componentDidMount()
메서드를 실행한다. 여기서 타이머를 설정하는 것이 좋겠다:
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
어떻게 타이머 ID를 this(this.timerID)
에 저장하는지 보자.
this.props
가 React 자체에 설정되고 this.state
가 특수한 의미를 갖지만, timer ID처럼 데이터 플로우에 해당되지 않는 무언가를 저장하고 싶다면, 수동으로 클래스에 부가적인 field를 편하게 추가해도 된다.
componeneWillUnmoun()
lifecycle 메서드에서 타이머를 없애보자:
componentWillUnmount() {
clearInterval(this.timerID);
}
이제 Clock
컴포넌트가 매초 동작하는 tick()
이라는 메서드를 구현해보자:
컴포넌트 로컬 state 업데이트를 위해 this.setState()
를 사용할 것이다:
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')
);
이제 시계는 매 초 똑딱인다.
빠르게 이게 어떻게 진행되는지 그리고 어떤 순서로 메서드가 호출되는지 뜯어보자:
<Clock />
이 ReactDOM.render()
로 넘겨질 때, React는 Clock
컴포넌트의 생성자를 호출한다. Clock
이 현재 시간을 보여주고 싶어하기 때문에, 이것은 현재시간을 포함하는 객체로 this.state
를 초기화한다. 이 state는 나중에 업데이트할 것이다.Clock
컴포넌트의 render() 메서드를 호출한다. 이것이 React가 화면에 무엇을 보여주어야 할 지 알게 되는 방법이다. React는 그리고 Clock
의 출력 렌더링과 일치하게 DOM을 업데이트한다.Clock
출력이 DOM에 삽입될 때, React는 componeneDidMount()
lifecycle 메서드를 호출한다. 이 안에서, Clock
컴포넌트는 브라우저가 컴포넌트의 tick()
메서드를 매초 호출하기 위해 타이머를 설정하도록 요청한다.tick()
메서드를 호출한다. 이 안에서 Clock
컴포넌트는 현재시간을 포함하는 객체로 setState()
를 호출함으로써 UI 업데이트를 진행한다. setState()
를 호출함으로써, React는 state가 변했다는 것을 알게 되고 무엇이 화면에 반영되어야하는지 알기 위해 render()
메소드를 재호출한다. 이때, render()
메서드 안의 this.state.date
가 바뀌고, 렌더링 출력은 업데이트된 시간을 포함한다. React는 DOM을 일치하게 업데이트한다.Clock
컴포넌트가 DOM으로부터 삭제된 적이 있다면, React는 ComponeneWillUnmount()
lifecycle 메서드를 호출해서 타이머가 멈추도록 한다.setState()
에 대해 알아야 할 3가지가 있다.
예를 들어, 이것은 컴포넌트를 리렌더링하지 않는다.
// Wrong
this.state.comment = 'Hello';
대신 setState()
를 사용해야 한다:
// Correct
this.setState({comment: 'Hello'});
this.state
는 생성자에서 유일하게 할당할 수 있다.
React는 성능을 위해 여러개의 setState()
호출해 단일 업데이트를 진행할 수 있다.
this.props
그리고 this.state
는 비동기적으로 업데이트되기 때문에, 다음 state를 계산하기 위해 그 값에 의존해서는 안된다.
예를 들어, 이 코드는 counter를 업데이트하는데 실패한다:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
이를 고치기 위해, 객체가 아닌 함수를 받아들이는 setState()
의 두번째 form을 사용하자. 함수는 첫번째 인자로 이전 state를 받고, 업데이트 시점의 props를 두번째 인자로 적용된다:
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
위에서 화살표 함수를 사용했지만, 일반 함수로도 작동한다.
// Correct
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
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.posts
를 내부에 놓지만, 완전히 this.state.comments
로 대체된다.
부모, 자식 컴포넌트 둘다 어떤 컴포넌트가 stateful한지 stateless한지 알지 못한다. 그리고 그것이 함수인지 클래스인지도 신경쓰지 않는다.
이것이 state가 종종 local로 호출되거나 캡슐화되는 이유이다. 이것은 그 소유이거나, 설정되지 않은 어떤 컴포넌트의 접근도 허용하지 않는다.
컴포넌트는 그 state를 props로서 자식 컴포넌트에게 넘겨줄지 선택한다:
<FormattedDate date={this.state.date} />
FormattedDate
컴포넌트는 date
를 props로 받고, 이것이 Clock
의 state로부터 왔는지, Clock
의 props로부터 왔는지 손으로 쓰여진 것인지 알지 못한다:
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
이것을 보통 "top-down"혹은 "unidirectional" data flow라고 부른다. 어떤 state든지 어떤 특정한 컴포넌트에 소속되어있고, 그 state로부터 전달된 어떤 데이터나 UI든지 오직 그것이 있는 트리 "아래"에 있는 컴포넌트에만 영향을 줄 수 있다.
컴포넌트 트리를 props의 폭포라고 가정해본다면, 각각의 컴포넌트의 state는 마치 임의의 점에서 합쳐져 함께 아래로 흐르는 부가적인 수자원이다.
모든 컴포넌트가 완전히 독립되어있음을 보기 위해, 세개의 <Clock>
을 렌더링하는 App
컴포넌트는 생성해보자.
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
각각의 Clock
은 자체 타이머를 설정하고 독립적으로 업데이트 한다.
React 앱에서, 컴포넌트가 stateful한지 stateless한지 시간이 흐르면서 변화하는 컴포넌트 디테일 구현에서 고려된다. stateless 컴포넌트를 stateful 컴포넌트에서 사용할 수 있고, 그 반대도 마찬가지이다.