해석하며 공부하는 것을 목적으로 하기 때문에 다수의 의역, 오역이 있음을 미리 밝힙니다.
원본 : https://reactjs.org/docs/state-and-lifecycle.html
이 페이지는 리액트 컴포넌트의 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);
이번 섹션에서는, 재사용할 수 있고 캡슐화되어있는 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')
);
}
이것을 구현하기 위해서는, Clock
컴포넌트에 "state"를 추가해줘야 합니다.
State는 props와 비슷하지만, 비공개이고 컴포넌트에 의해 완전히 제어됩니다.
다섯가지 단계로 Clock
과 같은 함수 컴포넌트를 클래스로 전환할 수 있습니다:
1. 같은 이름의 ES6 클래스를 만들어서 React.Component
를 상속합니다.
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
클래스 인스턴스만 사용될 것입니다. 이것은 로컬 state와 생명주기 메소드와 같은 추가적인 기능을 사용해야 한다는 것을 의미합니다.
세 단계로 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 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')
);
다음으로, 우리는 Clock
에 타이머를 세팅하고 그 자체로 매 초마다 업데이트되도록 만들 예정입니다.
많은 컴포넌트가 포함된 어플리케이션에서, 컴포넌트가 삭제될 때 컴포넌트가 사용 중이던 자원을 해방하는 것이 매우 중요합니다.
Clock
이 처음 DOM에 렌더링될 때마다 타이머를 세팅하려고 합니다. 리액트에서는 이것을 "마운팅"이라고 합니다.
또한 Clock
에 의해 만들어진 DOM이 제거될 때마다 타이머를 없애려고 합니다. 리액트에서는 이것을 "언마운팅"이라고 합니다.
컴포넌트 클래스에서 특별한 메소드를 선언하여 컴포넌트가 마운트되거나 언마운트될 때 일부 코드를 실행할 수 있습니다.
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
(this.timerID
)에서 어떻게 타이머 ID를 저장하는지 주의해주세요.
this.props
가 리액트에 의해 설정되고 this.state
가 특수한 의미를 지녔음에도 timer ID와 같은 데이터 흐름에 참여하지 않는 무언가를 저장해야 한다면, 직접 클래스에 부가적인 공간을 더해줘도 됩니다.
생명주기 메소드 componentWillUnMount()
로 타이머를 분해해보도록 하겠습니다:
componentWillUnmount(){
clearInterval(this.timerID);
}
마침내, Clock
컴포넌트가 매 초 실행하는tick()
메소드를 구현해보도록 하겠습니다.
this.setState()
를 사용해서 컴포넌트의 로컬 state를 업데이트할 예정입니다.
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(){
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
이제 시계는 매초 똑딱거립니다.
메소드가 어떤 순서로 호출되는지에 대해서 빠르게 요약해보도록 하겠습니다:
<Clock/>
이 ReactDOM.render()
로 전달되면, 리액트는 Clock
컴포넌트의 생성자를 호출합니다. Clock이 현재 시간을 보여줘야하기 때문에, 현재 시간을 포함한 오브젝트 this.state
를 초기화합니다. 추후, 이 state를 업데이트합니다.Clock
컴포넌트의 render()
메소드를 호출합니다. 이것이 리액트가 스크린에 보여줘야 하는 것을 습득하는 방법입니다. 그러고나서 리액트는 Clcok
의 렌더링 결과에 해당하는 DOM 요소를 업데이트합니다. Clock
결과물이 DOM에 삽입될 때, 리액트는 생명주기 메소드 componentDidMount()
를 호출합니다. 이 메소드 안에, Clock
컴포넌트가 브라우저에 컴포넌트의 tick()
메소드를 매초 실행할 타이머를 설정해달라고 요청합니다.tick()
메소드를 호출합니다. 메소드 안에는 Clock
컴포넌트가 현재 시간을 포함한 오브젝트로 setState()
를 호출하여 UI 업데이트를 예정합니다. setState()
호출 덕분에, 리액트는 state가 변했다는 알게 되고, 어떤 것이 스크린에 있어야하는지 알기 위해서 render()
메소드를 다시 호출합니다. 이 단계에서 render()
메소드 안의 this.state.date
가 달라지며, 렌더링 결과물에 업데이트된 시간이 포함됩니다. 그에 따라 리액트는 DOM을 업데이트합니다.Clock
컴포넌트가 DOM에서 삭제되지 않는 한, 리액트는 생명주기 메소드 componentWillUnmout()
를 계속 호출하기 때문에 타이머는 멈추지 않습니다.setState()
에 대해서 알아야 할 세 가지가 있습니다.
아래의 예시는 컴포넌트를 다시 렌더링하지 못할 것입니다 :
//오답
this.state.comment='Hello';
대신에 setState()
를 사용하세요:
//정답
this.setState({comment:'Hello'});
this.state
를 할당할 수 있는 유일한 곳은 생성자입니다.
리액트는 성능을 위해 여러 가지의 setState()
호출을 하나의 업데이트로 묶습니다.
this.props
와 this.state
는 비동기적으로 업데이트되기 때문에, 다음 state를 계산할 때 이전 값들에 의존해서는 안됩니다.
아래의 코드는 counter에 업데이트 하는 것을 실패할 것입니다:
//오답
this.setState({
counter:this.state.counter+this.props.increment,
});
이것을 수정하기 위해서, 오브젝트보다는 함수를 받아들이는 두번째 형식의 setState()
를 사용합시다. 아래의 함수는 첫번째 인자로 이전 state를 받아오고, 그 때 업데이트가 적용된 props가 두번째 인자로 받아옵니다:
//정답
this.setState((state,props)=>({
counter:state.counter+props.increment
}));
위의 예시에서는 화살표 함수를 사용했지만 정규 함수식으로도 표현할 수 있습니다:
//정답
this.setState(function(state,props){
return{
counter:state.counter+props.increment
};
});
setState()
를 호출할 때, 리액트는 현재 state로 제공된 오브젝트를 합칩니다.
예를 들어, state는 여러가지 독립된 변수들을 포함할 수 있습니다:
constructor(props){
super(props);
this.state={
post:[],
comments:[]
};
}
이러한 state들을 분리된 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가 소유하고 설정한 컴포넌트 이외에는 어떠한 컴포넌트에도 접근할 수 없습니다.
컴포넌트는 컴포넌트의 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>;
}
이것은 일반적으로 "하향식"이나 "단방향"의 데이터 흐름이라고 불립니다. 모든 state는 항상 특정한 컴포넌트가 소유하고, 그러한 state로부터 파생된 어떤 데이터나 UI는 컴포넌트 하위 트리에만 영향을 미칩니다.
컴포넌트 트리구조를 props의 폭포수같은 형태로 상상해본다면, 각 컴포넌트의 state는 임의의 지점에서 만나지만 아래로 흐르는 또 다른 수원(water source)라고 볼 수 있습니다.
모든 컴포넌트가 정말로 독립되었다는 것을 보여주기 위해서, 세 개의 <Clock>
을 렌더링하는 App 컴포넌트를 만들었습니다.
function App(){
return(
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
각 Clock
은 각자의 타이머가 설정되어 있고, 독립적으로 업데이트합니다.
리액트 애플리케이션에서는 컴포넌트의 state 유무와 관계없이 매 순간 컴포넌트에 변화가 일어난다는 구현 사항을 고려합니다. state가 있는 컴포넌트 안의 state가 없는 컴포넌트를 사용할 수 있고, 그 반대로의 경우도 있을 수 있습니다.