2020/09/24 - [Web프런트엔드/리액트] - [React] 컴포넌트의 Lifecycle 생명주기 함수 - 이론
이번 글을 읽기 전에 꼭 이론 편을 보고 오시길 추천드리며, 될 수 있으면 이론 화면을 같이 보시는 것이 더욱 좋습니다. 그럼 실습에 들어갈 텐데 이번 코드는 좀 복잡할 수 있어, 실행 화면을 먼저 보여드리고 설명 후에 진행하겠습니다.
이번 실습은 일부로 잘 못된 코드도 넣었고 콘솔 로그가 많으니 실제로 실습을 해보시길 권해드립니다.
앞으로 나올 실습 코드를 넣으면 이런 초기 화면이 나오는데 로그가 중요하니 꼭 콘솔 창을 같이 보시길 바라며, 로그는 생명주기 함수의 호출 순서나 프로퍼티 값 스테이트 값 등이 나오므로 참고하시면 됩니다.
화면에는 총 두 개의 컴포넌트가 있는데 위쪽 컴포넌트는 props.update 값이 false이며 아래쪽은 true입니다. 한마디로 위쪽(초기화 컴포넌트)은 갱신 버튼이 작동하고 아래쪽(갱신된 컴포넌트)은 강제 갱신만 작동합니다. 갱신 버튼을 누르면 카운트가 증가하고 콘솔 창에 생명주기 함수 호출 로그가 나옵니다. 그럼 APP.js부터 가시죠!
// ..\test_project\src\App.js
import React, { Component } from "react";
import LifecycleComponent from "./components/LifecycleComponent";
class App extends Component {
render() {
return (
<div>
<LifecycleComponent name={"초기화 컴포넌트"} update={false} />
<LifecycleComponent name={"갱신된 컴포넌트"} update={true} />
</div>
);
}
}
export default App;
너무 간단하죠? 이제부터 만들어 볼 자식 컴포넌트를 두 개 생성하였습니다. 하나는 update 프로퍼티가 false 또 하나는 true입니다. 여기서 중요한 것은 클래스형 컴포넌트로 만든 LifecycleComponent가 두 개라는 것입니다.
즉 하나의 소스로 만든 두 개의 인스턴스가 생성된다는 점입니다. 그럼 LifecycleComponent.jsx 만들어 보시죠!
// ..\test_project\src\components\LifecycleComponent.jsx
import React from "react";
import PropTypes from "prop-types";
class LifecycleComponent extends React.Component {
// 1. 초기화등을 하는 생성자
constructor(props) {
super(props);
// 스테이트 초기화
this.state = {
isUpdate: props.update, // 상위 프로퍼티값
isDestroy: false,
countNum: 0,
timeSec: 0,
intervalID: 0,
};
console.log(
`constructor 호출 props : ${props.update} , isUpdate : ${this.state.isUpdate}`
);
}
// 2. 반환값으로 스테이트 변경
static getDerivedStateFromProps(nextProps, prevState) {
console.log(
`getDerivedStateFromProps 호출 props : ${nextProps.update} ,
isUpdate : ${prevState.isUpdate} , isDestroy : ${prevState.isDestroy}`
);
// 들어올 때 마다 카운트
const nextState = prevState;
nextState.countNum++;
return nextState;
}
// 3. 화면 출력 (렌더링)
render() {
console.log(
`render 호출 props : ${this.props.update} , isUpdate : ${this.state.isUpdate}`
);
// null값 반환시 컴포넌트 소멸
if (this.state.isDestroy === true) return null;
return (
<div>
<p>
{this.props.name} , isUpdate : {String(this.state.isUpdate)} ,
countNum : {this.state.countNum}
</p>
<button onClick={clickUpdate.bind(this)}>갱신</button>
<button onClick={clickDestroy.bind(this)}>소멸</button>
<button onClick={clickForceUpdate.bind(this)}>강제 갱신</button>
<p>경과 시간 : {this.state.timeSec}초</p>
</div>
);
}
// 4. 출력(마운트) 후에 딱 한번 호출
componentDidMount() {
// 매초 마다 함수 실행 (잘 못된 코드)
startTimeSec = startTimeSec.bind(this);
this.state.intervalID = setInterval(startTimeSec, 1000);
console.log(
`componentDidMount 호출 props : ${this.props.update} ,
isUpdate : ${this.state.isUpdate} , intervalID : ${this.state.intervalID}`
);
}
// 5. 갱신을 할지 말지 결정하는 함수
shouldComponentUpdate(nextProps, nextState) {
console.log(
`shouldComponentUpdate 호출 props : ${nextProps.update} ,
isUpdate : ${nextState.isUpdate}`
);
// 반환값이 true인 경우 갱신
return nextProps.update !== nextState.isUpdate;
}
// 6. 실제 화면 갱신 전에 상황을 반환값(sanpshot)으로 넘김
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log(
`getSnapshotBeforeUpdate 호출 props : ${prevProps.update} ,
isUpdate : ${prevState.isUpdate}`
);
return prevState.timeSec;
}
// 7. 실제 화면 갱신 후에 6번 반환값을 snapshot인자 값으로 받음
componentDidUpdate(prevProps, prevState, snapshot) {
console.log(
`componentDidUpdate 호출 props : ${prevProps.update} ,
isUpdate : ${prevState.isUpdate} , snapshot : ${snapshot}`
);
}
// 8. 컴포넌트가 소멸 되기 바로 직전에 호출
componentWillUnmount() {
// 4번 함수의 setInterval() 해제
clearInterval(this.state.intervalID);
console.log(
`componentWillUnmount 호출 props : ${this.props.update} ,
isUpdate : ${this.state.isUpdate} , intervalID : ${this.state.intervalID}`
);
}
}
// 이하 클래스 외부의 함수들은 동작은 하지만 잘못된 코드들 입니다.
// 자세한 설명은 본문에서 하겠지만 전부 클래스 내부로 옴겨야 합니다.
function clickUpdate() {
this.setState({ isUpdate: true });
console.log(
`clickUpdate 호출 props : ${this.props.update} , isUpdate : ${this.state.isUpdate}`
);
}
function clickForceUpdate() {
this.setState({ isUpdate: true });
this.forceUpdate();
console.log(
`clickForceUpdate 호출 props : ${this.props.update} , isUpdate : ${this.state.isUpdate}`
);
}
function clickDestroy() {
this.setState({ isUpdate: true, isDestroy: true });
console.log(
`clickDestroy 호출 props : ${this.props.update} , isUpdate : ${this.state.isUpdate} ,
isDestroy : ${this.state.isDestroy}`
);
}
function startTimeSec() {
this.state.timeSec++;
}
LifecycleComponent.propTypes = {
name: PropTypes.string,
update: PropTypes.bool,
};
export default LifecycleComponent;
코드가 길어 보이지만 대부분 콘솔 로그가 많고 로직은 많지 않습니다. 코드를 천천히 보시거나 주석을 읽으시면 이해가 가시리라 생각됩니다.? 그래도 간단하게 설명을 드리자면, 부모 컴포넌트에서 받은 프로퍼티(props.update)로 분기 처리를 하여 각 컴포넌트의 스테이트 값을 변경하고 화면에 출력합니다.
바로 전 포스팅에 설명했던 8가지의 생명주기 함수를 전부 사용하여, 간단한 로직 구현 및 로그가 남도록 하였습니다. 그럼 중요한 부분을 한번 더 가시죠!
// 이하 클래스 외부의 함수들은 동작은 하지만 잘못된 코드들 입니다.
// 자세한 설명은 본문에서 하겠지만 전부 클래스 내부로 옴겨야 합니다.
function clickUpdate() {
this.setState({ isUpdate: true });
console.log(
`clickUpdate 호출 props : ${this.props.update} , isUpdate : ${this.state.isUpdate}`
);
}
function clickForceUpdate() {
this.setState({ isUpdate: true });
this.forceUpdate();
console.log(
`clickForceUpdate 호출 props : ${this.props.update} , isUpdate : ${this.state.isUpdate}`
);
}
function clickDestroy() {
this.setState({ isUpdate: true, isDestroy: true });
console.log(
`clickDestroy 호출 props : ${this.props.update} , isUpdate : ${this.state.isUpdate} ,
isDestroy : ${this.state.isDestroy}`
);
}
function startTimeSec() {
this.state.timeSec++;
}
동작 이벤트를 처리하고 setState 메서드를 사용하는 부분인데, 전부 클래스 안쪽이 아닌 외부에 정의되어 있습니다. bind 했기 때문에 문제없어 보이지만, "갱신된 컴포넌트"의 강제 갱신 버튼을 눌러도 경과 시간이 0초에 멈춰 있고
"초기화 컴포넌트"의 강제 갱신 버튼을 계속 눌러보면 경과 시간이 1초가 아닌 2초 단위로 올라갑니다.
import React from "react";
import PropTypes from "prop-types";
class LifecycleComponent extends React.Component {
...
// 4. 출력(마운트) 후에 딱 한번 호출
componentDidMount() {
// 매초 마다 함수 실행 (잘 못된 코드)
startTimeSec = startTimeSec.bind(this);
this.state.intervalID = setInterval(startTimeSec, 1000);
console.log(
`componentDidMount 호출 props : ${this.props.update} ,
isUpdate : ${this.state.isUpdate} , intervalID : ${this.state.intervalID}`
);
}
...
}
...
function startTimeSec() {
this.state.timeSec++;
}
...
문제의 부분을 보면 startTimeSec함수에서 변숫값을 1식 증가시키고, 클래스 안의 componentDidMount함수에서 1초 단위로 setInterval 하고 있는데, 로직으로는 별 이상이 없어 보이지만 사실은 크게 잘 못 사용된 코드입니다.
다시 App.js로 돌아가 보면 LifecycleComponent를 두 개 추가했습니다. 즉, 클래스 형태로 보자면 인스턴스가 두 개 생성된 것인데 startTimeSec함수는 클래스 외부의 독립된 함수여서, 각각의 인스턴스(컴포넌트)에서 돌아가지 않고 처음으로 생성된 인스턴스(초기화 컴포넌트)에 몰빵(?)해서 돌아가고 있습니다. 그래서 두 번째 컴포넌트는 0초이고 첫 번째 컴포넌트는 2초 단위로 올라가게 됩니다. bind의 this범위만이 아니라 이렇듯 함수의 정의 위치도 중요합니다.
또 하나 잘 못 사용된 부분은 this.state의 변수들을 setState 메서드가 아닌, 직접(directly) 값을 변경한 부분입니다. 이 부분은 콘솔 창에도 경고 메시지가 표시되어서 근방 찾을 수 있지만 놓치기 쉬운 부분입니다. 그럼 정리를 한번 하죠!
제가 일부로 잘 못된 코드를 올리고 설명을 드리는 이유는, 리액트의 개발 철학인 컴포넌트의 재사용 설명에 있습니다. 리액트 컴포넌트는 절대 하나의 소스로 된 컴포넌트를 한 번만 사용하지 않습니다.
당연하게도 클래스형 컴포넌트는 수많은 인스턴스가 생성될 것이고 또한 수많은 데이터가 맞물려 돌아갈 것이니, 위에서 설명한 부분들을 잘 숙지해야 하고 조금 귀찮더라도 리액트 시스템에서 추천하는 방법을 적극 수용해서 사용하는 것이 좋다고 생각합니다.
오늘은 말이 너무 길었습니다.
그럼 여기까지 입니다.
참 쉽죠?
가 아닌가...
'웹 프론트엔드 > React 리액트 (기초)' 카테고리의 다른 글
[React] 아이디, 비번 입력 Input 컴포넌트를 만들어보자 (0) | 2020.09.29 |
---|---|
[React] 컴포넌트의 DOM 관련 이벤트 - 무한 scroll 예제 (0) | 2020.09.28 |
[React] 컴포넌트의 Lifecycle 생명주기 함수 - 이론 (0) | 2020.09.24 |
[React] 컴포넌트의 형태를 분류해보자 (0) | 2020.09.22 |
[React] 컴포넌트의 데이터 관리 ③ - 콜백 함수 (0) | 2020.09.21 |
댓글