State 와 라이프사이클
지금까지 우리는 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 컴포넌트를 만드는 방법에 대해 배웁니다.
자체 타이머를 설정하고 매 초마다 스스로 업데이트합니다.
시계가 어떻게 보이는 지 캡슐화하는 것부터 시작합니다.
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"));
이걸 구현하기 위해, Clock 컴포넌트에 “state”를 추가할 필요가 있습니다.
State 는 props 와 비슷하지만 컴포넌트에 의해 완전히 제어되며 private 속성입니다.
이전에 언급 했던 대로 클래스로 정의한 컴포넌트에는 몇가지 추가 기능이 있습니다.
로컬 state 는 클래스에서만 사용 가능한 기능입니다.
함수를 클래스로 변환
Clock 같은 함수형 컴포넌트를 클래스로 변환하려면 다섯 단계를 진행합니다.
- ES6 class 를 같은 이름으로 만들고, React.Component 를 확장합니다.
- 비어있는 render() 메서드를 하나 추가합니다.
- 함수의 바디를 render() 메서드 안으로 옮깁니다.
- render() 바디 내에서 props 를 this.props 로 바꿉니다.
- 남아있는 빈 함수 선언문을 제거합니다.
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
Clock 은 이제 함수 대신 클래스로 정의합니다.
이를 통해 로컬 state 나 라이프사이클 훅 같은 추가 기능을 사용할 수 있습니다.
Class 에 로컬 state 추가하기
date 를 props 에서 state 로 옮기기 위해서 세 단계를 진행합니다.
- 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 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 와 함께 기본 생성자를 호출합니다.
-
요소에서 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
이라고 부릅니다.
그리고 DOM 에서 Clock 을 삭제했을 때 타이머를 해제 하려고 합니다. React 에서 이를 unmounting
이라고 부릅니다.
컴포넌트가 마운트 (mount) 되고 언마운트 (unmount) 될 때 특정 코드를 실행하기 위해 컴포넌트 클래스에 특별한 메서드를 선언할 수 있습니다.
// Life Cycle
// Render: componentWillMount() -> render() -> componentDidMount()
// Update componentWillReceiveProps() -> shouldComponentUpdate() -> componentWillUpdate() -> render() -> componentDidUpdate()
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() 훅은 컴포넌트 출력이 DOM 에 렌더링 된 이후 동작합니다.
이 부분이 타이머를 설정하기 좋은 지점입니다.
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
this 에 timer ID 를 어떻게 저장하는 지 살펴봅시다.
렌더링과 관련된 데이터 흐름에서, this.props 는 React 에 의해 설정되고 this.state 는 특별한 의미를 갖고 있습니다.
반면 위와 같이 데이터 흐름에 참여하지 않는 무언가를 저장할 때 클래스에 직접 필드를 추가하는 것도 가능합니다.
이제 componentWillUnmount() 라이프사이클 훅에서 타이머를 종료할 것입니다.
componentWillUnmount() {
clearInterval(this.timerID);
}
마지막으로 Clock 컴포넌트에서 매 초마다 동작하는 tick() 이라는 메서드를 구현해봅시다.
this.setState() 를 사용해서 컴포넌트 로컬 state 에 대한 업데이트를 예약합니다.
// 리액트는 최초 렌더를 제외하고는 변화가 있어야 화면을 렌더합니다.
// componentDidMount는 1초마다 tick을 호출하고,
// tick은 state를 변경해줍니다.
// state가 변경되었기에 다시 렌더가 되고,
// 렌더가 완료된 후에 clearInterval이 이루어집니다.
class Clock extends React.Component {
// 클래스 필드를 사용하면 생성자를 사용하지 않고도 초기 값을 지정할 수 있다.
// state = { date: new Date() }
constructor(props) {
super(props);
this.state = { date: new Date() };
}
componentDidMount() {
this.timerID = setInterval(() => this.tick(), 1000);
// tick을 1초에 한번씩 호출하면 setState를 호출하니 화면을 다시 그려주게 되는 셈입니다.
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
// 1. 상태를 바꿔주고, 2. 화면이 간접적으로 다시 그려지도록 해줍니다.
date: new Date()
});
}
// 상태로 부터 화면이 어떻게 그려져야 하는지를 render 메소드에 서술합니다.
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(<Clock />, document.getElementById("root"));
이제 시계는 매 초 깜빡입니다.
어떤 작업을 했는 지와 메서드가 호출되는 순서를 간단히 요약해봅시다.
이 ReactDOM.render() 에 전달될 때, React 는 Clock 컴포넌트의 생성자 함수를 호출합니다.
Clock 이 현재 시각을 화면에 보여주어야 하기 때문에, 현재 시각을 포함하는 this.state 객체를 초기화합니다.
이 state 는 추후 업데이트됩니다.
React 가 Clock 컴포넌트의 render() 메서드를 호출합니다.
이를 통해 React 는 화면에 무엇을 보여줘야 하는지 알아냅니다.
그 다음 React 는 DOM 을 갱신해서 Clock 의 렌더링 출력과 일치시킵니다.
Clock 출력이 DOM 에 주입되었을 때, React 는 componentDidMount() 라이프 훅을 호출합니다.
그 안에서 Clock 컴포넌트는 브라우저에게 컴포넌트의 tick() 메서드를 초당 한 번씩 호출하는 타이머를 설정하라고 명령합니다.
브라우저에서 매 초마다 tick() 메서드를 호출합니다.
그 안에서 Clock 컴포넌트는 현재 시각을 갖고 있는 객체를 가지고
setState() 를 호출하여 UI 업데이트를 예약합니다.
setState() 호출 덕분에, React 는 상태가 변경된 걸 알게 됐고,
(setState()를 호출하면 화면이 다시그려지게 된다는 것도 기억해야 합니다.)
render() 메서드를 다시 한 번 호출해 화면에 무엇을 표시해야 할지 알 수 있습니다.
이번에는, render() 메서드 내의 this.state.date 가 달라지므로 바뀐 시간이 출력에 포함됩니다.
React 는 그에 따라 DOM 을 업데이트합니다.
만약 Clock 컴포넌트가 DOM 에서 삭제된다면,
React 는 componentWillUnmount() 라이프사이클 훅을 호출하기 때문에 타이머가 멈춥니다.
State 바르게 사용하기
- State 를 직접 수정하지 마세요
이 코드는 컴포넌트를 다시 렌더링하지 않습니다.
// Wrong
this.state.comment = "Hello";
대신, setState() 를 사용하세요.
// Correct
this.setState({ comment: "Hello" });
this.state 를 할당할 수 있는 유일한 장소는 생성자 함수 내부입니다.
- State 업데이트는 비동기일 수 있습니다
React 는 성능을 위해 여러 setState() 호출을 한 번의 작업으로 묶어서 처리하는 경우가 있습니다.
this.props 및 this.state 가 비동기로 업데이트될 수 있기 때문에,
다음 state 를 계산할 때 이 값을 신뢰해서는 안됩니다.
예를 들어, 카운터를 업데이트하는 이 코드는 실패할 수 있습니다.
// Wrong
this.setState({
counter: this.state.counter + this.props.increment
});
이 문제를 해결하기 위해 객체가 아닌 함수를 받는 두 번째 형식의 setState() 를 사용할 수 있습니다.
이 함수는 이전 state 를 첫 번째 인수로 받고, 두 번째 인수로 업데이트가 적용 될 때 props 를 받습니다.
// 이전 상태로부터 새 상태를 계산할 때는 콜백 형식의 setState를 사용해야 합니다.
// 객체를 바로 반환받음에도 중괄호를 괄호로 덮어줘야 합니다. 안그러면 에러가 발생합니다.
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
위 예제에서는 arrow function 을 사용했지만, 평범한 함수도 동작합니다.
// Correct
this.setState(function(prevState, props) {
return {
counter: prevState.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.posts 는 그대로 두지만, this.state.comments 는 완전히 대체합니다.
- 데이터는 아래로 흐릅니다
부모 컴포넌트나 자식 컴포넌트는 특정 컴포넌트의 state 유무를 알 수 없으며,
해당 컴포넌트가 함수나 클래스로 선언되었는 지 알 수 없습니다.
이것이 바로 state 가 ‘지역적이다’ 혹은 ‘캡슐화되었다’고 하는 이유입니다.
State 를 지정해 준 컴포넌트 외의 다른 컴포넌트에서는 state 에 접근할 수 없습니다.
컴포넌트는 자신의 state 를 props 로서 자식 컴포넌트에 내려줄 수 있습니다.
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
이는 사용자 정의 컴포넌트에서도 마찬가지로 동작합니다.
<FormattedDate date={this.state.date} />
FormattedDate 컴포넌트는 props 에서 date 를 받지만</h2> 이 값이 Clock 의 상태인지, Clock 의 props 인지,</h2> 혹은 타이핑된 것인지 알 수 없습니다.</h2>
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
이런 데이터 흐름을 보통 “하향식(top-down)” 혹은 “단방향(unidirectional)” 데이터 흐름이라고 합니다.
모든 state 는 항상 특정 컴포넌트가 가지며, 해당 state 에서 파생된 모든 데이터와
UI 는 트리의 “아래(below)“에 있는 컴포넌트에만 영향을 미칩니다.
컴포넌트 트리를 props 의 폭포라고 상상해보면,
각 컴포넌트의 상태는 임의의 지점에서 추가되는 물과 비슷하고 또한 아래로 흐릅니다.
모든 컴포넌트가 실제로 격리되어있음을 보여주기 위해,
세 개의
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
각 Clock 은 자체적으로 타이머를 생성하고 독립적으로 바뀝니다.
React 앱에서, 컴포넌트가 state 를 갖고 있는지(stateful) 또는 갖지 않는지(stateless)는
구현 세부 사항으로 간주되어 시간이 지남에 따라 바뀔 수 있습니다.
state 를 가진 컴포넌트 내부에서 state 가 없는 컴포넌트를 사용할 수 있으며, 그 반대 경우도 마찬가지입니다.