리액트(React) 학습자를 위한 기초지식
컴포넌트 생명주기(Lifecycle of Components) - #2 Updating (2/3)
Updating
리액트 컴포넌트 생명주기의 첫번째 단계인 Mounting 단계를 거치면 Updating 단계로 들어갑니다. Updating은 Mounting과정을 거쳐 DOM을 형성한 컴포넌트에 state값이나 props값을 변경할 때 발생합니다. 지난글인 [컴포넌트 생명주기 - #1 Mounting]에서 예시로 적어둔 것 처럼, componentDidMount() 함수를 이용해 state값을 변경했습니다. 이미 DOM을 형성한 컴포넌트에 state값을 변경하니 getDerivedStateFromProps() 함수와 render() 함수가 실행됐습니다.
이렇듯 state값 또는 props값이 변경되었을 때 실행되는 단계가 Updating입니다. 그러면 Updating에서는 어떤 함수들이 어떤 순서대로 작동하는지 보겠습니다.
- getDrivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
여기에서도 역시 render() 함수 작성은 필수며, 나머지 함수들은 필요에 따라 함수 내부를 마음대로 작성해도 됩니다. 그럼 Updating 단계에서 호출되는 함수들을 조금 더 자세히 살펴보겠습니다.
getDerivedStateFromProps
getDerivedStateFromProps() 함수는 state값을 props값에서 가져오는 함수입니다. 또한 이 함수는 Mounting 단계 뿐 아니라 Updating 단계에서도 작동합니다. 다만 Mounting 단계에서는 2번째로 호출되며, Updating 단계에서는 첫번째로 호출되는 차이점이 있습니다.
이 함수는 파라미터값으로 props와 state가 들어가며, return값으로 변경할 state값을 넣어주면 됩니다. 예제를 작성해보겠습니다.
Header.jsx
import React, { Component } from 'react';
class Header extends Component{
constructor(props){
super(props);
this.state = {
favoriteColor: 'red',
}
console.log('constructor() 실행 및 color => ', this.state.favoriteColor);
}
static getDerivedStateFromProps(props, state){
console.log('get..()실행 및 state값 => ', state.favoriteColor);
return{
favoriteColor: props.favCol
}
}
changeColor = () => {
this.setState({
favoriteColor: "blue",
})
console.log('changeColor() 실행');
}
render(){
console.log('render() 실행 및 color => ', this.state.favoriteColor);
return(
<div>
<h1>My Favoriate Color is {this.state.favoriteColor}</h1>
<button onClick={this.changeColor}>Change Color</button>
</div>
);
}
}
export default Header;
App.js
import React from 'react';
import Header from './components/Header';
function App() {
return (
<div>
<Header favCol="pink" />
</div>
);
}
export default App;
실행화면1
실행화면2
컴포넌트를 실행하면 처음에는 실행화면1처럼 화면이 출력됩니다. 그리고 버튼을 클릭하면 blue로 변경될 줄 알았는데, 변경되지 않고 pink로 값이 유지됐습니다. 이유는 이렇습니다. 이미 Mounting 단계에서 형성된 DOM에 버튼을 눌러 state값을 변경하는 과정에서 Updating 단계로 넘어갑니다. Updating단계에서 먼저 호출되는 함수는 getDrivedStateFromProps()입니다. 클릭버튼을 눌렀을 때 getDrivedStateFromProps() 함수 내 console.log()를 출력해보면, return되기 이전 state값은 버튼을 클릭했을 때 변경된 blue값으로 변경되었습니다. 하지만 이 함수가 실행되어 return된 후 값을 보면 props값으로 설정한 pink로 다시 출력되어서 나옵니다. 전형적인 낄끼빠빠 함수네요.
shouldComponentUpdate
shouldComponentUpdate() 함수는 리액트 컴포넌트의 업데이트(state값 혹은 props값의 변경)를 허락할지 불허할지 결정(?)하는 함수입니다. 업데이트를 허락하면 state나 props값이 바뀌면서 컴포넌트가 렌더링이 됩니다. 만약 불허한다면 shouldComponentUpdate() 함수까지만 호출되고 컴포넌트는 렌더링이 되지 않습니다. 이 함수의 return값은 boolean타입으로, Update를 진행하고자 한다면 true값을, 진행하지 않으려면 false를 작성하면 됩니다. 참고로 이 함수의 default값은 true입니다.
Header.jsx
import React, { Component } from 'react';
class Header extends Component{
constructor(props){
super(props);
this.state = {
favoriteColor: 'red',
}
console.log('constructor() 실행');
}
shouldComponentUpdate(){
console.log('shouldComponentUpdate() 실행');
return false;
}
changeColor = () => {
this.setState({
favoriteColor: "blue",
})
console.log('changeColor() 실행');
}
render(){
console.log('render() 실행 및 color => ', this.state.favoriteColor);
return(
<div>
<h1>My Favoriate Color is {this.state.favoriteColor}</h1>
<button onClick={this.changeColor}>Change Color</button>
</div>
);
}
}
export default Header;
실행화면
shouldComponentUpdate() 함수 return값을 false로 주었더니, 버튼을 클릭해도 update가 진행되지 않아 렌더링이 되지 않았습니다. 이제 이 함수 return값을 true로 바꿔주겠습니다.
header.jsx
shouldComponentUpdate(){
console.log('shouldComponentUpdate() 실행');
return true;
}
실행화면
true로 바꾼 후 버튼을 클릭했더니 red에서 blue로 바뀌었습니다.
render
render() 함수는 당연히 Updating 단계에서도 실행됩니다. 왜냐하면 변경된 state값 혹은 props값을 다시 DOM에 나타내야 하기 때문입니다. 실행화면은 따로 보여드리지 않고 코드만 작성하겠습니다.
header.jsx
import React, { Component } from 'react';
class Header extends Component{
constructor(props){
super(props);
this.state = {
favoriteColor: 'red',
}
console.log('constructor() 실행');
}
changeColor = () => {
this.setState({
favoriteColor: "blue",
})
console.log('changeColor() 실행');
}
render(){
console.log('render() 실행 및 color => ', this.state.favoriteColor);
return(
<div>
<h1>My Favoriate Color is {this.state.favoriteColor}</h1>
<button onClick={this.changeColor}>Change Color</button>
</div>
);
}
}
export default Header;
getSnapshotBeforeUpdate
스냅샷이 뭔지 궁굼해서 찾아봤습니다.
- 스냅샷은 짧은 순간의 기회를 이용하여 찍는 사진을 말한다.
- 스냅샷은 과거의 한 때 존재하고 유지시킨 컴퓨터 파일과 디렉터리의 모임이다.
- Wikipedia
getSnapshotBeforeUpdate() 함수는 Update가 발생하기 이전 DOM의 state값과 props값을 가져오는 함수입니다. 이를 스냅샷 의미와 관련지어서 설명하자면, Update가 발생하기 전에 형성된 DOM을 '찰칵'하고 사진을 찍어 가져와 여기에 있는 state값과 props값을 가져오는 함수라고 설명할 수 있겠습니다.
이 함수의 return값은 componentDidUpdate() 함수의 3번째 파라미터로 전달됩니다. 따라서 getSnapshotBeforeUpdate() 함수를 사용한다면, componentDidUpdate() 함수를 사용해야 합니다. 그렇지 않으면 "getSnapshotBeforeUpdate() should be used with componentDidUpdate()"라고 에러가 발생합니다. 또한 return을 명시하지 않으면 "A snapshot value (or null) must be returned. You have returned undefined."라고 에러가 발생합니다.
header.jsx
import React, { Component } from 'react';
class Header extends Component{
constructor(props){
super(props);
this.state = {
favoriteColor: 'red',
}
console.log('constructor() 실행');
}
componentDidMount(){
console.log('componentDidMount() 실행');
setTimeout(() => {
this.setState({
favoriteColor: "black"
})
}, 2000);
}
getSnapshotBeforeUpdate(prevProps, prevState){
console.log('getSnapshotBeforeUpdate() 실행');
document.getElementById("div1").innerHTML =
"Before the update, the favorite color was " + prevState.favoriteColor;
return null;
}
componentDidUpdate(){
console.log('componentDidUpdate() 실행');
document.getElementById("div2").innerHTML =
"But the updated favorite color is " +
this.state.favoriteColor;
}
render(){
console.log('render() 실행 및 color => ', this.state.favoriteColor);
return(
<div>
<h1>My Favoriate Color is {this.state.favoriteColor}</h1>
<div id="div1"></div><br/>
<div id="div2"></div>
</div>
);
}
}
export default Header;
실행화면1
2초 뒤에 다음 화면으로 전환됩니다.
실행화면2
componentDidMount()로 2초 뒤 state값이 변경(Updating)되도록 작성했습니다. Updating 단계 순서대로 render() 함수가 실행되어 변경된 state값이 적용되었습니다. 이후 getSnapshotBeforeUpdate() 함수가 실행되어 Updating 이전의 state값을 가져오고, 연이어 componentDidUpdate() 함수가 실행되면서 update 된 state값을 가져왔습니다.
componentDidUpdate
componentDidUpdate() 함수는 컴포넌트가 DOM에 업데이트 된 이후에 호출되는 함수입니다. 예제를 작성하겠습니다.
header.jsx
import React, { Component } from 'react';
class Header extends Component{
constructor(props){
super(props);
this.state = {
favoriteColor: 'red',
}
console.log('constructor() 실행');
}
componentDidMount(){
console.log('componentDidMount() 실행');
setTimeout(() => {
this.setState({
favoriteColor: "black"
})
}, 2000);
}
componentDidUpdate(){
console.log('componentDidUpdate() 실행');
setTimeout(() => {
document.getElementById("myDiv").innerHTML =
"The updated favorite color is " +
this.state.favoriteColor;
}, 2000);
}
render(){
console.log('render() 실행 및 color => ', this.state.favoriteColor);
return(
<div>
<h1>My Favoriate Color is {this.state.favoriteColor}</h1>
<div id="myDiv"></div><br/>
</div>
);
}
}
export default Header;
실행화면
componentDidMount() 함수에서 state값이 변경되었기 때문에 Updating 단계가 발생했습니다. 따라서 render() 함수가 실행되고, 이후 componentDidUpdate() 함수가 실행되었습니다.
여기까지가 컴포넌트 내 state값 혹은 props값이 변경되었을 때 발생하는 컴포넌트 생명주기의 Updating 단계입니다. 다음 글에서 리액트 생명주기의 마지막 단계인 Unmounting에 대해서 알아보겠습니다.
추가사항
getSnapshotBeforeUpdate() 함수와 componentDidUpdate() 함수 예제
[출처 : Velopert. ⌜누구든지 하는 리액트 5편: LifeCycle API⌟. https://velopert.com/3631]
ScrollBox.jsx
import React, { Component } from 'react';
import './ScrollBox.css';
class ScrollBox extends Component {
id = 2;
state = {
array: [1]
};
handleInsert = () => {
this.setState({
//...은 전개 연산자(Spread Operator)
array: [this.id++, ...this.state.array]
});
};
getSnapshotBeforeUpdate(prevProps, prevState){
if(prevState.array !== this.state.array){
const { scrollTop, scrollHeight } = this.list;
//이 함수 return값은 componentDidUpdate() 함수의 3번째 파라미터값으로 전달됩니다.
return {
scrollTop,
scrollHeight
};
}
}
componentDidUpdate(prevProps, prevState, snapshot){
if(snapshot){
//진짜 3번째 파라미터로 넘어왔는지 확인
console.log('snapshot => ', snapshot);
console.log('snapshot의 Height => ', snapshot.scrollHeight);
//신기한 this.list
const { scrollTop } = this.list;
if(scrollTop !== snapshot.scrollTop) return;
const diff = this.list.scrollHeight - snapshot.scrollHeight;
this.list.scrollTop += diff;
}
}
render() {
const rows = this.state.array.map(number => (
<div className="row" key={number}>
{number}
</div>
));
//리스트가 잘 생기는지 확인
console.log('rows => ', rows);
return (
<div>
<div
ref={ ref => {
this.list = ref;
}}
className="list"
>
{rows}
</div>
<button onClick={this.handleInsert}>Click</button>
</div>
);
}
}
export default ScrollBox;
ScrollBox.css
.list{
height: 10rem;
overflow: auto;
padding: 1rem;
border: 1px solid black;
}
.row{
padding: 1rem;
margin: 0.5rem;
border: 1px solid black;
}
App.js
import React from 'react';
import ScrollBox from './components/ScrollBox';
function App() {
return (
<div>
<ScrollBox />
</div>
);
}
export default App;
실행화면
getSnapshotBeforeUpdate() 함수를 활용해서 버튼클릭시 스크롤이 생겨도 화면이 스크롤을 따라가는 것이 아니라 고정되게 하였습니다. 끗!
댓글