이 포스팅은 https://reactjs.org/docs/state-and-lifecycle.html 에 있는 포스팅을 번역한 것입니다. 오역이나 의역이 있을 수 있습니다. 지적해주시면 확인 후 바로 정정하겠습니다.
original source of this posting is from https://reactjs.org/docs/state-and-lifecycle.html If the original author requests deletion, it will be deleted immediately.
Translated by Jake Seo (서진규)
- https://velog.io/@jakeseo_me
- https://github.com/n00nietzsche
리액트 공식문서로 배워보자 #5, State와 라이프사이클!
이 페이지에서는 리액트 컴포넌트 내부의 state와 라이프사이클에 대해 설명할 것입니다. 상세한 컴포넌트 API 레퍼런스는 여기서 볼 수 있습니다.
이전 섹션에서 똑딱거리는 시계 예제를 기억하시나요? 리액트 엘리먼트에서, 우리는 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);
CodePen에서 직접 해보기
이 섹션에서, 우리는 어떻게 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);
CodePen에서 직접 해보기
하지만, 위 코드에서는 결정적인 요구사항 하나를 놓쳤습니다. Clock
이 타이머를 세팅하고 UI를 매초마다 업데이트 하는 것은 Clock
구현의 세부사항이어야 합니다.
우리는 이상적으로 Clock
을 한번 작성하고, Clock
이 스스로 업데이트되길 원합니다. 다음 코드와 같이요.
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
이것을 구현하기 위해, 우리는 Clock
컴포넌트에 "state"를 추가해주어야 할 필요가 있습니다.
state는 props와 비슷합니다. 근데 private한 특성을 갖고 있고, 컴포넌트에 의해 완전히 컨트롤됩니다.
다음 5단계를 거치면 Clock
과 같은 함수형 컴포넌트를 클래스형 컴포넌트로 변환할 수 있습니다.
React.Component
를 상속받는 ES6 클래스를 만듭니다.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>
);
}
}
CodePen에서 직접 해보기
Clock
은 이젠 함수가 아닌 클래스로 정의되어 있습니다.
render
메소드는 업데이트가 일어날 때마다 호출될 것입니다. 하지만 우리가 <Clock />
을 같은 DOM node안에 렌더링하는 한, Clock
클래스의 하나의 인스턴스만이 사용되겠죠. 이러한 일은 우리가 local state나 라이프사이클 메소드와 같은 추가적인 기능을 사용할 수 있도록 해줍니다.
우리는 3단계에 거쳐 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>
);
}
}
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을 <Clock />
엘리먼트에서 제거합니다.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')
);
CodePen에서 직접 해보기
다음으로, 우리는 Clock
이 자체 타이머를 갖고 스스로 매 초마다 업데이트 하도록 만들겠습니다.
많은 컴포넌트를 가진 어플리케이션에서, 컴포넌트가 제거될 때, 컴포넌트에 의해 묶여진 리소스를 풀어주는 것은 매우 중요합니다.
우리는 Clock
이 DOM에 처음으로 렌더링 될 때마다, 타이머를 세팅하길 원합니다. 이러한 순간을 리액트에서 "mounting"이라고 표현합니다.
또 우리는 Clock
에 의해 생성된 DOM이 제거될 때마다, timer를 clear하길 원합니다. 이러한 순간을 리액트에서 "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 method)"라 부릅니다.
componentDidMount()
메소드는 컴포넌트의 출력 결과물이 DOM에 랜더링 된 이후에 동작합니다. 그래서 여기는 타이머를 세팅하기 좋은 지점입니다.
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
우리가 this
에 어떻게 타이머 ID를 바로 저장했는지 기억해두세요.
this.props
는 리액트 자체에서 설정되고, this.state
는 특별한 의미를 가지지만, 만일 데이터 플로우에 참여하지 않는 무언가를 저장하길 원한다면 (timer ID와 같은), 추가적인 필드를 클래스에 언제든 수동으로 추가할 수 있습니다.
componentWillUnmoun()
라이프사이클 메소드 내에서 우리는 타이머를 끌(Clear) 수 있습니다.
componentWillUnmount() {
clearInterval(this.timerID);
}
마지막으로 Clock
컴포넌트를 매 초마다 실행시킬 tick()
메소드를 구현할 것입니다.
tick()
메소드는 컴포넌트의 local 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')
);
CodePen에서 직접 해보기
이제 매 초마다 Clock
이 똑딱거릴 것입니다.
무슨 일이 일어나는 건지 그리고 메소드가 호출되는 순서에 대해서 빠르게 한번 되짚어봅시다.
<Clock />
이 ReactDOM.render()
로 넘겨졌을 때, 리액트는 Clock
컴포넌트의 생성자를 호출합니다. Clock
컴포넌트는 현재 시간을 보여줄 필요가 있기 때문에 현재 시간을 포함하는 오브젝트로 this.state
를 초기화합니다. 우리는 나중에 this.state
를 업데이트 하게 될 것입니다.Clock
컴포넌트의 render()
메소드를 호출하게 됩니다. render()
메소드를 통해 리액트는 화면에 무엇을 보여줘야 할지에 대해서 알게됩니다. 다음으로 리액트는 Clock
의 render 출력에 맞추기 위해 DOM을 업데이트합니다.Clock
의 출력이 DOM에 삽입됐을 때, 리액트는 componentDidMount()
라이프사이클 메소드를 호출합니다. 내부에서, Clock
컴포넌트는 브라우저에게 tick()
메소드를 매 초마다 호출하기 위해 타이머를 세팅해달라고 요청합니다.tick()
메소드를 호출합니다. 내부적으로, Clock
컴포넌트는 현재 시간을 담고 있는 오브젝트를 setState()
메소드를 통해 계속 할당함으로써, UI 업데이트를 스케쥴링합니다. setState()
를 호출하는 덕분에, 리액트는 state가 바뀐 것을 알게됩니다. 그리고 render()
메소드가 다시 스크린에 보여줄 내용을 재설정하기 위해 호출됩니다. 이번에는 render()
메소드 내부의 this.state.date
가 달라집니다 그리고 그래서 렌더링되는 출력에도 업데이트된 시간이 포함됩니다. 리액트는 DOM을 이에 따라 업데이트합니다.Clock
컴포넌트가 DOM으로부터 제거된다면, 리액트는 componentWillUnmount()
라이프사이클 메소드를 호출하고 타이머는 멈추게 됩니다.setState()
에 대해 알아야 하는 3가지 내용이 있습니다.
예를 들면, 이러한 방식은 컴포넌트를 재렌더링하지 못합니다.
// Wrong
this.state.comment = 'Hello';
대신, setState()
를 사용하세요.
// Correct
this.setState({comment: 'Hello'});
this.state
를 할당할 수 있는 유일한 장소는 생성자 뿐입니다.
리액트는 성능을 위해서 여러개의 setState()
호출을 하나의 업데이트에 배치(batch)합니다.
this.props
와 this.state
는 아마 비동기적으로 업데이트되기 때문에, 다음 상태에 대한 연산을 하기 위해서 이러한 값들에 의존하면 안됩니다.
예를 들면, 이 코드는 카운터를 업데이트하는데 실패할 수 있습니다.
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
위 코드를 고치기 위해, 오브젝트가 아니라 함수를 받아들이는 두번째 형태의 setState()
를 사용하세요. 그 함수는 이전 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()
를 호출할 때, 리액트는 우리에게서 받은 오브젝트를 현재 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
});
});
}
병합은 얕은(shallow) 수준에서 이뤄집니다. 그래서 this.setState({comments})
는 this.state.posts
를 그대로의 상태로 납둡니다. 하지만 this.state.comments
를 완전히 대체합니다.
부모 자식 컴포넌트 둘 다 특정 컴포넌트가 state가 있는 형태인지(stateful) state가 없는 형태인지(stateless) 알 수 없습니다. 그리고 함수형 컴포넌트인지 클래스형 컴포넌트인지에 대해 신경쓰지도 않습니다.
state가 지역적(local) 혹은 캡슐화(encapsulated)되어있다고 불리는 이유가 바로 이것입니다. 그 state를 가지고 있고 세팅한 컴포넌트 이외에는 어떠한 다른 컴포넌트도 그 state에 접근할 수 없습니다.
컴포넌트는 state를 props를 통해 자식 컴포넌트로 내려보낼 수도 있습니다.
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
사용자정의(user-defined) 컴포넌트에서도 물론 이 방법은 동작합니다.
<FormattedDate date={this.state.date} />
FormattedDate
컴포넌트는 props에 date
를 받을 것입니다. 그리고 이 date
가 Clock
의 state에서 왔는지, props에서 왔는지, 아니면 그냥 손으로 타이핑된 건지는 알 수 없을 겁니다.
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
CodePen에서 직접 해보기
이건 일반적으로 "탑다운(top-down)" 혹은 "단방향성(unidirectional)" 데이터 플로우라고 불립니다. 어떤 state는 특정한 컴포넌트에 의해 소유됩니다. 그리고 어떤 그 컴포넌트의 state로부터 끌어낸 데이터나 UI는 트리에서 오직 그 "하위(below)"에 있는 컴포넌트에게만 영향을 미칠 수 있습니다.
모든 컴포넌트가 독립되어 있다는 것을 보여주기 위해, 3개의 <Clock>
을 렌더링하는 App
컴포넌트를 만들 수도 있습니다.
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
CodePen에서 직접 해보기
각각의 Clock
은 자신이 가진 타이머를 세팅하고 독립적으로 업데이트 됩니다.
리액트 어플리케이션에서, 컴포넌트가 상태가 있는지 상태가 없는지(stateful or stateless)는 매번 변할 수 있는 컴포넌트의 구현 디테일로 고려됩니다. stateful 컴포넌트 내부에 stateless 컴포넌트를 사용할 수도 있고 반대도 가능(vice versa)합니다.