🗂 목차
State and Lifecycle
이 페이지에서는 React 컴포넌트의 State와 Lifecycle 의 개념을 소개합니다. 이전 글에서 작성했던 시계 예제를 봅시다. ReactDOM.render()
를 호출해서 UI를 업데이트하는 방법을 배웠습니다.
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);
하지만 타이머를 설정하고 매 초 UI를 업데이트 하는 것이 Clock
의 구현 세부사항이 되어야 한다는 사실을 놓치고 있습니다. 이상적으로 우리는 코드를 한번만 작성하고 Clock
이 스스로 업데이트하길 원합니다:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
이것을 실행 하기 위해서 Clock
컴포넌트에 “state” 를 추가 해야 합니다.
State는 Props와 비슷하지만 private 하고 컴포넌트에 의해 조작됩니다.
Converting a Function to a Class
5 단계로 Clock
과 같은 함수형 컴포넌트를 클래스형 컴포넌트로 바꿀 수 있습니다.
- 같은 이름으로 된
React.Component
를 확장하는ES6 class
를 작성합니다. - 빈
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
이 함수형에서 클래스형으로 바뀌었습니다.
render()
메소드는 업데이트가 될 때 마다 호출 될 것 입니다. 하지만 Clock
이 같은 DOM 노드에서 렌더링 되기 때문에 하나의 Clock
인스턴스만 사용됩니다. 이것은 우리에게 state
, lifecycle
같은 추가 기능을 사용하게 해줍니다.
Adding Local State to a Class
3 단계를 거쳐서 데이터를 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>
);
}
}
2. 초기 this.state
를 지정하는 class constructor (생성자) 를 추가합니다.
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는 항상 기본 생성자에 의해 호출됩니다.
3. 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
의 타이머를 설정하고 매 초 업데이트 하도록 하겠습니다.
Adding Lifecycle Methods to a Class
많은 컴포넌트가 있는 애플리케이션에서 컴포넌트가 제거될 때 컴포넌트의 리소스들을 확보하는 것은 매우 중요합니다.
우리는 DOM에 Clock
이 처음 렌더링 될 때 타이머가 설정 되기를 원합니다. 이것을 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 메소드” 라고 부릅니다.
componentDidMount()
메소드는 컴포넌트의 출력값이 DOM에 렌더링이 된 후에 실행합니다. 이 부분이 타이머를 설정하기 좋은 곳입니다.
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
this
(this.timerID
)에서 timer ID를 저장하는 방법을 주의하세요.
this.props
는 React에 의해 스스로 설정하고 this.state
가 특별한 의미를 갖지만, 타이머 ID와 같이 데이터 흐름 안에 포함되지 않는 어떤 것을 보관할 필요가 있다면 자유롭게 클래스에 수동으로 부가적인 필드를 추가해도 됩니다.
componentWillUnmount
메소드에서 타이머를 지워보겠습니다.
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
이 현재 시간을 보여줘야하기 때문에 현재 시간을 포함한 객체를 초기화합니다. 우리는 이 state를 나중에 업데이트 합니다.- 다음 React가
Clock
컴포넌트의render()
메소드를 호출합니다. 이것을 통해 React가 무엇을 화면에 띄어야 할지 알게 됩니다. 다음 React가Clock
의 렌더링 출력물에 맞춰서 DOM을 업데이트합니다. Clock
의 출력물이 DOM에 추가될 때, React는componentDidMount()
메소드를 호출합니다. 그 안에서는,Clock
컴포넌트는 1초의 한번씩tick()
메소드를 호출하기 위한 타이머를 설정하도록 브라우저에게 요청합니다.- 매 초마다 브라우저가
tick()
메소드를 호출합니다. 그 안에서는Clock
컴포넌트는setState()
에 현재 시간을 포함하는 객체를 호출해서 UI 업데이트를 진행합니다.setState()
호출 덕분에 React는 state의 변화를 알게 됩니다. 그리고 화면에 표시할 내용을 알기 위해서render()
메소드를 다시 호출합니다. 이번에는render()
메소드의this.state.date
는 다르기 때문에 렌더링의 출력물은 업데이트된 시간을 포함할 것입니다. React는 그에 맞춰 DOM을 업데이트 합니다. - 만약
Clock
컴포넌트가 DOM에서 한번이라도 삭제된다면, React는 타이머를 멈추기 위해서componentWillUnmount()
를 호출합니다.
Using State Correctly
setState()
에 대해 알아야할 3가지가 있습니다.
Do Not Modify State Directly
이것은 re-rendering이 일어나지 않습니다.
// Wrong
this.state.comment = 'Hello';
대신, setState()
를 사용해야 합니다.
// Correct
this.setState({comment: 'Hello'});
this.state
를 지정할 수 있는 유일한 공간은 바로 생성자 입니다.
State Updates May Be Asynchronous
React는 성능을 위해 여러 setState()
호출을 단일 업데이트로 한꺼번에 처리할 수 있습니다.
this.props
와 this.state
가 비동기적으로 업데이트될 수 있기 때문에 다음 state를 계산할 때 해당 값에 의존해서는 안 됩니다.
이 코드는 counter 업데이트에 실패할 수 있습니다.
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
고치기 위해서 객체대신 함수형으로된 setState()
의 두번째 양식을 사용합니다. 이 함수는 첫번째 인자로 이전의 state를 받습니다. 두번째 인자로 업데이트된 props를 받습니다.
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
위에서는 arrow function을 사용했지만, 정규 함수도 작동합니다.
// Correct
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
State Updates are Merged
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
는 완전히 대체됩니다.
The Data Flows Down
부모 컴포넌트나 자식 컴포넌트 모두 특정 컴포넌트의 state유무를 알 수 없고, 그들이 함수나 클래스로 정의 되었는지에 대해서 관심을 가질 필요가 없습니다. 이것이 state가 대게 로컬 또는 캡슐화로 불리는 이유입니다. state를 보유하거나 설정하지 않은 다른 컴포넌트가 접근할 수 없습니다. 컴포넌트가 본인의 state를 자식 컴포넌트에게 props 로 전달 할 수도 있습니다.
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
이것은 사용자 정의 컴포넌트에서도 사용할 수 있습니다.
<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” ) 데이터 흐름으로 불립니다. 모든 state는 항상 어떤 특정 컴포넌트가 갖습니다. 그리고 그 state에서 파생된 모든 데이터 또는 UI는 트리 구조에서 “아래” 컴포넌트에게만 영향을 미칠 수 있습니다.
트리구조가 props들의 폭포라고 상상하면 각 컴포넌트의 state는 임의의 점에서 만나지만 동시에 아래로 흐르는 부가적인 소스 (source) 라고 할 수 있습니다.
독립된 모든 컴포넌트를 보기 위해선, 우리는 3개의 Clock
을 렌더링 하는 App
컴포넌트를 만들 수 있습니다.
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}ReactDOM.render(
<App />,
document.getElementById('root')
);
각 Clock
컴포넌트는 스스로 타이머를 설정하고 독립적으로 업데이트합니다.
React 앱에서 컴포넌트가 상태가 있는지 없는지는 시간에 따른 컴포넌트의 실행 세부 사항 결정됩니다. 무상태 컴포넌트 안에서 유상태 컴포넌트를 사용할 수 있고, 그 반대의 경우도 가능합니다.
📚 정리 및 참고 자료
- Lifecycle
- State와 Props는 비동기적으로 작동합니다.
- State를 변경할 때는
setState()
를 사용해야 합니다.