LifeCycle


이번 포스팅은 기본적인 Class형 Component의 LifeCycle에 대해 포스팅 하겠습니다.


component의 lifecycle은 크게 3가지로 분류할수 있습니다.


1. Component 생성

2. 이미 생성된 Component에서 props 혹은 state 가 변화

3. Component 삭제


출처 : https://velopert.com/1130


1. component 생성 lifecycle


우선 컴포넌트 생성시 발생하는 lifecycle에 대해 살펴보겠습니다.


class형 component가 생성할때에는 총 4개의 cycle이 작동을 하게 됩니다.


위 순서대로 작동을 하게 되는데요,


코드를 작성해보기전에 설명 드리겠습니다.


다른 class에서 사용하던것과 마찬가지고 class가 생성되면서 작동하는 생성자 함수 입니다.

이곳에서 초기 state를 지정할수 있습니다. 


component가 Dom에 생성되기 직전, 즉 mount 되기 직전에 실행됩니다.

이 함수는 서버 렌더링에서 호출되는 유일한 라이프 사이클입니다. 

contstructor대신 사용하셔도 괜찮습니다.


component 렌더 함수입니다.


component가 Dom에 생성이 완료된후, 즉 mount 완료 후, 불려지는 함수입니다.

대부분 이곳에서 다른 프레임 워크를 연동하거나 ajax 요청을 하게 됩니다.


그럼 소스코드에서 위 함수들을 생성한후 확인해보겠습니다.


src/App.tsx

import * as React from 'react';
import './App.css';

import StatelessComponent from './Stateless';

const logo = require('./logo.svg');

interface AppProps {
name: string;
}

interface AppState {
age: number;
}

class App extends React.Component<AppProps, AppState> {

constructor(props: {name: string}) {
console.log('App constructor');
super(props);
this.state = {
age: 31
};
this._addAge = this._addAge.bind(this);
}

componentWillMount() {
console.log('App componentWillMount');
}

componentDidMount() {
console.log('App componentDidMount');
}

render() {

console.log('App render');

const {name} = this.props;
const {age} = this.state;

return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<h1>Hello {name}</h1>
<h1>age : {age}</h1>
<button onClick={this._addAge}>나이 추가</button>
<StatelessComponent name="React-TS">Children</StatelessComponent>
</div>
);
}

private _addAge(): void {
this.setState({
age: this.state.age + 1
});
}

}

export default App;


앞에서 한 코드에서 약간의 수정을 해주었습니다.


이미 constructor와 render함수는 있으므로 componentWillMount() 함수와 componentDidMount() 함수를 추가해주고

브라우져 콘솔로 작동 순서를 확인하기 위해 console.log를 추가하였습니다.


한번 실행하고, 브라우져의 콘솔창을 열어보겠습니다.




* 아마 그냥 실행하신다면 lint에서 console.log를 쓸수 없다고 나올것입니다.

  이때는 tslint.json 에 가서


"no-console": [

true,
"error",
"debug",
"info",
"time",
"timeEnd",
"trace"
],


"no-console" 항목의 log를 삭제해주세요.




실행 화면입니다.




콘솔창을 확인해 보면,


constructor

componentWillMount

render

componentDidMount


순서로 콘솔이 찍힌것을 확인할수 있습니다.


이 순서가 component의 생성 lifecycle임을 확인할수 있습니다.


2. 이미 생성된 Component에서 props 혹은 state 가 변화 lifecycle


여기에서는 총 5개의 cycle이 존재합니다.



하나하나 설명 드리겠습니다.


이 함수는 props가 변경되었을때,  그 변경된 값을 받기 전 호출됩니다.

이때 Props에 따라 state가 변경된다면, 이곳에서 setState함수를 사용하여 state를 변경하셔도 됩니다.

이때 불려지는 setState함수에 대한 추가적인 렌더링은 발생하지 않습니다.


이 함수는 props 혹은 state가 변경 되었을때, 새롭게 렌더링을 할지 말지를 결정하는 함수입니다.

return 값으로 boolean type을 사용하며 true일경우 새로운 렌더링을, false일경우에는 렌더링을 하지 않습니다.

만일 이 함수를 쓰지 않는다면, default값이 true이므로 props나 state 변경시 무조건 새롭게 렌더링을 하게 됩니다.


이 함수 component가 update되기 직전에 호출되는 함수입니다.

여기서는 setState함수를 사용하지 마세요. 무한 루프에 빠지게 됩니다.

만일 shouldComponentUpdate 에서 false값을 return 하였다면, 이 함수는 호출되지 않습니다.


렌더링 함수입니다.


component가 새롭게 렌더링을 마친뒤 호출되는 함수입니다. 이곳에서 현재의 값과 이전 값을 비교하여 작업을 할수 있습니다.

componentWillUpdate와 마찬가지로 shouldComponentUpdate 에서 false값을 return 하였다면, 이 함수는 호출되지 않습니다.



이번엔 코드를 src/App.tsx에 새로운 class형 컴포넌트를 추가하여 살펴보겠습니다.


우선 src/App.tsx를 변경하겠습니다.


import * as React from 'react';
import './App.css';

import Parent from './Parent';
import StatelessComponent from './Stateless';

const logo = require('./logo.svg');

interface AppProps {
}

interface AppState {
name: string;
}

class App extends React.Component<AppProps, AppState> {

constructor(props: {name: string}) {
console.log('App constructor');
super(props);
this.state = {
name: 'jaro'
};
}

componentWillMount() {
console.log('App componentWillMount');
}

componentDidMount() {
console.log('App componentDidMount');
}

render() {

console.log('App render');

const { name } = this.state;

return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<Parent name={name}/>
<StatelessComponent name="React-TS">Children</StatelessComponent>
</div>
);
}
}

export default App;


기존에 name과 age, 그리고 나이를 추가하는 요소들을 Parent 컴포넌트로 바꿨습니다.

그리고 App의 state.name을 Parent의 props 데이터로 넘겨주는 형식입니다.


src/Parent.tsx 파일입니다.

import * as React from 'react';

interface ParentProps {
name: string;
}

interface ParentState {
age: number;
}

class Parent extends React.Component<ParentProps, ParentState> {

constructor(props: {name: string}) {
console.log('Parent constructor');
super(props);
this.state = {
age: 31
};
this._addAge = this._addAge.bind(this);
}

componentWillMount() {
console.log('Parent componentWillMount');
}

componentDidMount() {
console.log('Parent componentDidMount');
}

render() {
console.log('Parent render');
const {name} = this.props;
const {age} = this.state;
return (
<div className="Parent">
<h1>Hello {name}</h1>
<h1>age : {age}</h1>
<button onClick={this._addAge}>나이 추가</button>
</div>
);
}

private _addAge(): void {
this.setState({
age: this.state.age + 1
});
}

}

export default Parent;


실행시키면 이전과 다른것이 없는 화면이 나올것입니다.


다만, 콘솔창을 확인해보면 



이렇게 나와있는것을 확인할수 있습니다.


즉 최상위 component에서 render가 불리면서 하위 component의 생성이 모두 완료 되어야 ( DidMount까지 끝나야 )

최상위 component의 componentDidMount가 불리는것을 확인할수 있습니다.


그럼 Parent Component에 각 함수들을 추가해보겠습니다.


componentWillReceiveProps(nextProps: ParentProps) {
console.log(
`Parent componentWillReceiveProps : ${JSON.stringify(nextProps)}`
);
}

shouldComponentUpdate(nextProps: ParentProps, nextState: ParentState): boolean {
console.log(
`Parent shouldComponentUpdate : ${JSON.stringify(nextProps)}, ${JSON.stringify(nextState)}`
);
return true;
}

componentWillUpdate(nextProps: ParentProps, nextState: ParentState) {
console.log(
`Parent componentWillUpdate : ${JSON.stringify(nextProps)}, ${JSON.stringify(nextState)}`
);
}

componentDidUpdate(prevProps: ParentProps, prevState: ParentState) {
console.log(
`Parent componentDidUpdate : ${JSON.stringify(prevProps)}, ${JSON.stringify(prevState)}`
);
}


이미 이전 포스팅에서 state를 변경하는 버튼을 추가했으므로 바로 실행하여 확인해보도록 하겠습니다.


실행하고 콘솔창을 열고 나이 추가 버튼을 클릭해보세요.




버튼을 누르면 cycle대로 작동하는것을 확인할수 있습니다.

다만 props가 아닌 state를 변경하였기 때문에 componentWillReceiveProps는 작동하지 않았네요.


그럼 props를 변경해보겠습니다.


쉬운 방법으로 Parent component의 상위 component인 App 에 함수를 추가해주세요.


private _changeName(): void {
const name = (this.state.name === 'jaro') ? 'react' : 'jaro';
this.setState({
name: name
});
}


버튼을 누를때마다 Parent component로 전달되는 name의 데이터가 jaro, react로 바뀔것입니다.


constructor에 binding도 추가해줍니다.


this._changeName = this._changeName.bind(this);


render에 버튼을 추가해주세요.


<button onClick={this._changeName}>이름 변경</button>


실행해보겠습니다.




버튼을 누르면 componentWillReceiveProps 부터 cycle이 작동하는것을 확인할수 있습니다.


그렇다면 위에서 말씀드린 shouldComponentUpdate를 살펴보도록 하겠습니다.

이 함수는 return 값을 갖고, 그 값은 boolean 이라고 말씀드렸습니다.


함수를 보겠습니다.


shouldComponentUpdate(nextProps: ParentProps, nextState: ParentState): boolean {
console.log(
`Parent shouldComponentUpdate : ${JSON.stringify(nextProps)}, ${JSON.stringify(nextState)}`
);
return true;
}


return 값이 true로 되어있습니다.


한번 false로 바꾸고 저장한뒤 나이 추가 버튼을 눌러보겠습니다.




아무리 버튼을 눌러도 업데이트 되지 않습니다.


즉 만일 props나 state가 변경되었을때 이곳에서 분기를 생성하여 새롭게 렌더링을 할지 안할지를 결정할수 있습니다.


이 상태에서는 이름 변경 버튼을 눌러도 업데이트는 이루어지지 않습니다.



3. Component 삭제


이때 불려지는 lifecycle 함수는 1개입니다.


이 함수는 Dom이 Unmount되어 파기되기 바로 직전에 호출되며, 이 단계에서 타이머 무효화, 네트워크 요청 취소 또는 componentDidMount 에서 생성된 Dom 요소들의 정리등을 수행하는것이 좋습니다.


그럼 간단하게 코드로 확인해보겠습니다.


src 폴더에 secret.tsx 파일을 생성하고

아래 코드를 삽입해주세요.


import * as React from 'react';

interface SecretProps {
}

interface SecretState {
}

class Secret extends React.Component<SecretProps, SecretState> {

constructor(props: {name: string}) {
console.log('Secret constructor');
super(props);
}

componentWillMount() {
console.log('Secret componentWillMount');
}

componentDidMount() {
console.log('Secret componentDidMount');
}

componentWillReceiveProps(nextProps: SecretProps) {
console.log(Secret componentWillReceiveProps : ${JSON.stringify(nextProps)}`);
}

shouldComponentUpdate(nextProps: SecretProps, nextState: SecretState): boolean {
console.log(`Secret shouldComponentUpdate : ${JSON.stringify(nextProps)}, ${JSON.stringify(nextState)}`);
return false;
}

componentWillUpdate(nextProps: SecretProps, nextState: SecretState) {
console.log(`Secret componentWillUpdate : ${JSON.stringify(nextProps)}, ${JSON.stringify(nextState)}`);
}

componentDidUpdate(prevProps: SecretProps, prevState: SecretState) {
console.log(`Secret componentDidUpdate : ${JSON.stringify(prevProps)}, ${JSON.stringify(prevState)}`);
}

componentWillUnmount() {
console.log('Secret componentWillUnmount');
}

render() {
console.log('Secret render');
return (
<div className="Secret">
<h1>Secret</h1>
</div>
);
}
}

export default Secret;


그저 lifecycle만 확인하기 위한 컴포넌트이므로 아무것도 없습니다.


이번엔 src/App.tsx 파일에 이 컴포넌트를 추가해 주도록 하겠습니다.


import * as React from 'react';
import './App.css';

import Parent from './Parent';
import Secret from './Secret';
import StatelessComponent from './Stateless';

const logo = require('./logo.svg');

interface AppProps {
}

interface AppState {
name: string;
secret: boolean;
}

class App extends React.Component<AppProps, AppState> {

constructor(props: {name: string}) {
console.log('App constructor');
super(props);
this.state = {
name: 'jaro',
secret: true
};
this._changeName = this._changeName.bind(this);
this._changeSecret = this._changeSecret.bind(this);
}

componentWillMount() {
console.log('App componentWillMount');
}

componentDidMount() {
console.log('App componentDidMount');
}

render() {

console.log('App render');

const { name, secret } = this.state;

return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<button onClick={this._changeName}>이름 변경</button>
<Parent name={name}/>
<StatelessComponent name="React-TS">Children</StatelessComponent>
{ secret ?
<Secret /> :
<h1 />
}
<button onClick={this._changeSecret}>비밀</button>
</div>
);
}

private _changeName(): void {
const name = (this.state.name === 'jaro') ? 'react' : 'jaro';
this.setState({
name: name
});
}

private _changeSecret(): void {
const secret = (this.state.secret) ? false : true;
this.setState({
secret: secret
});
}

}

export default App;


App의 state에 secret이라는 값을 추가하였습니다.

이 값이 true이면 secret컴포넌트를 mount하고, false이면 unmount할 계획입니다.

interface AppState {
name: string;
secret: boolean;
}


this.state = {
name: 'jaro',
  secret: true
};


secret 값을 변경하는 함수를 추가해주겠습니다.

이전값이 true였으면 false로, false였으면 true로 바꿔줍니다.

private _changeSecret(): void {
const secret = (this.state.secret) ? false : true;
this.setState({
secret: secret
});
}


constructor에 바인딩을 해주세요.

this._changeSecret = this._changeSecret.bind(this);


이전에는 render할때 name만 사용했지만, 이제는 secret도 사용할 것이므로 추가해주세요

const { name, secret } = this.state;


그리고 secret 값에 따라 렌더유무를 결정하도록 해주겠습니다.

{ secret ?
<Secret />
<h1 />
}


마치 조건부 삼항연산자와 같은 모습인데요

이 문항은 react conditional-rendering 으로, 상태값에 따라 유동적으로 렌더링을 하는것입니다.

방식은 일반적으로 사용하는 Javascript에서 사용하는 조건문과 동일합니다.

https://facebook.github.io/react/docs/conditional-rendering.html


자 그럼 실행시켜보겠습니다.


비밀 버튼을 눌러보겠습니다.


그리고 콘솔을 확인해주세요.



비밀이 사라지고


콘솔이 많이 생겼습니다.


이중에 아래서 2번째 Secret componentWillUnmount 가 보이시나요?

component가 unmount 되었다는 의미입니다.



* 추가적으로 여기서 Parent가 왜 새롭게 렌더링 되는지 이상하여 생각해보았습니다.

App에서 setState -> App의 shouldComponentUpdate -> App의 componentWillUpdate ->

App의 render가 실행되면서 이미 Parent component가 mount되어 있으니 componentWillReceiveProps 함수부터 실행된것으로 보입니다.

물론 넘겨주는 데이터는 변화가 없지만요. ( 추가적으로 props를 넘겨주지 않고 defaultProps로 하거나 아예 props 없이 해봤지만 역시 새롭게 렌더링을 하는것을 확인하였습니다. ) 

즉 상위 component에서 render가 발생하면, 하위 component는 이미 mount된 상태라면 componentWillReceiveProps 부터, unmount된 상태면 생성을 시작하는것으로 보입니다.


그래서 저는 Parent의 의미없는 재 렌더링을 막기 위해 src/Parent.tsx 에서

shouldComponentUpdate의 return 값을 조절하였습니다.

return(this.state.age !== nextState.age || this.props.name !== nextProps.name) ? true : false;


이번 포스팅에서는 react component의 lifecycle에 대해 알아보았습니다.


소스코드는 https://github.com/JaroInside/tistory-react-typescript-study/tree/7.lifecycle

에서 확인하실수 있습니다.


터미널에서

git clone https://github.com/JaroInside/tistory-react-typescript-study.git


cd tistory-react-typescript-study


npm install ( yarn install )


git checkout 7.lifecycle


감사합니다.



참고 슬라이드 - http://slides.com/woongjae/react-with-typescript-1#/
참고 동영상 - https://www.youtube.com/playlist?list=PLV6pYUAZ-ZoHx0OjUduzaFSZ4_cUqXLm0

블로그 이미지

Jaro

대한민국 , 인천 , 남자 , 기혼 , 개발자 jaro0116@gmail.com , https://github.com/JaroInside https://www.linkedin.com/in/seong-eon-park-16a97b113/

,