리액트의 라이프 사이클은 컴포넌트가 생성되고, 업데이트되고, 제거되는 과정에서 발생하는 여러 단계를 말한다. 주로 세 가지의 단계로 나눌 수 있다.
함수형 컴포넌트에서 useEffect를 써본 경험이 많기 때문에 저 세단계가 존재한다고 예상을 하긴했다. 하지만 깊게 알지는 못했기 때문에 이번 기회에 블로깅하면서 정리를 해보려한다.
함수형 컴포넌트와는 달리 클래스형 컴포넌트는 라이프 사이클과 상태 관리를 기본적으로 처리할 수 있다고한다.
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
// 컴포넌트가 처음 마운트될 때 실행되는 로직
}
componentDidUpdate() {
// 컴포넌트가 업데이트될 때 실행되는 로직
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
위 코드에서 우리가 함수형 컴포넌트를 사용할 때 Hook을 Import해 라이프 사이클과 상태를 관리하는 것과 달리 클래스형 컴포넌트의 경우
componentDidMount()
,componentDidUpdate()
,this.state = { count: 0 }
같은 코드에서 Import문 없이 자체적으로 관리하는 것을 볼 수 있다.
원래 React의 16.8버전 이전에는 useState와 useEffect같은 Hook들이 없었다. 따라서 당시에 함수형 컴포넌트는 상태가 없는 단순한 UI를 렌더링하는 정적인 컴포넌트였다. 그래서 자체적으로 상태와 라이프 사이클을 관리할 수 있는 클래스형 컴포넌트를 많이 사용했다. 하지만 Hook이 도입된 이후에는 두가지를 관리할 수 있고, 보일러 플레이트도 가벼운 함수형 컴포넌트를 많이 사용하기 시작했다.
import { useState, useEffect } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// 컴포넌트가 마운트될 때 실행됨
console.log('Component mounted');
return () => {
// 컴포넌트가 언마운트될 때 실행됨
console.log('Component unmounted');
};
}, []);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
위 코드를 보면 useState와 useEffect 두가지 Hook을 Import해서 자체적이지 않은 방법으로 두가지를 관리를 하고 있다.
이무튼 이런 역사를 기반으로 생각해보면 기본적으로 함수형 컴포넌트는 두가지를 관리하지 못하지만 Hook을 통해 관리를 할 수 있다고 생각하면 될 듯 하다.
새로운 컴포넌트가 생성되어 DOM에 삽입되는 단계이다. 단 한 번만 발생하며, 초기 렌더링이라고도 많이 말하는 듯 하다. 나는 예시를 참 좋아한다. 예시가 없으면 머릿속에서 글이 구조화 되기가 쉽지 않기 때문이다. 따라서 코드 예시를 먼저 들고 설명을 해보겠다.
import React from "react";
class ComponentDidMount extends React.Component {
constructor(props) {
super(props);
console.log('Constructor called');
this.state = {
count: 0
};
}
static getDerivedStateFromProps(props, state) {
console.log('getDerivedStateFromProps called');
return null;
}
componentDidMount() {
console.log('componentDidMount called');
}
incrementCount = () => {
this.setState(prevState => ({
count: prevState.count + 1
}));
};
render() {
console.log('render called');
return (
<div>
<h1>Counter App</h1>
<p>Count: {this.state.count}</p>
<button onClick={this.incrementCount}>Increment</button>
</div>
);
}
}
export default ComponentDidMount;
constructor 메서드는 해당 단계에서 가장 먼저 호출되는 메서드다. 컴포넌트의 상태를 초기화하고, props를 사용할 수 있게 해준다. 또한 컴포넌트 내에서 사용될 이벤트 핸들러 메서드를 바인딩하는 데 사용된다.
해석해보면 props로부터 파생된 상태를 가져온다는 걸 알 수 있듯이, props의 변화를 기반으로 상태를 업데이트할 수 있도록 해준다.
인자를 보면 Props, State 두 가지가 있는데 처음에 State가 왜 들어가지? 싶었다. 생각해보면 부모로부터 props를 내려받고 그 값으로 초기화되는 state가 있다면 업데이트 시 두 값이 같다면 불필요한 렌더링을 유발하니, 두 값을 비교하고 업데이트를 하기 때문인 듯 하다.
잘못 사용하면 많은 오류가 발생할 수 있어 초보자들은 피하는 것이 좋다고 한다.
JSX로 HTML을 작성하고 DOM에 삽입하는 단계이다.
컴포넌트가 처음으로 렌더링된 후(render() 이후)에 호출된다. 함수형 컴포넌트에서 의존성 배열이 빈 useEffect를 생각하면 편할 듯 하다. 따라서 네트워크 요청 등의 사이드 이펙트를 관리하는데 적합하다.
컴포넌트가 업데이트되거나 다시 렌더링될 때 발생한다. props나 state가 변경될 때 트리거된다. 마찬가지로 먼저 예시 코드를 보자
import React from "react";
class UpdatingExample extends React.Component {
constructor(props) {
super(props);
this.state = {
name: 'John',
changed: false
};
console.log('Constructor called');
}
static getDerivedStateFromProps(props, state) {
console.log('getDerivedStateFromProps called');
return null;
}
shouldComponentUpdate(nextProps, nextState) {
console.log('shouldComponentUpdate called');
return true;
}
getSnapshotBeforeUpdate(nextProps, nextState) {
console.log('getSnapshotBeforeUpdate called');
return null;
}
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate called');
}
changeName = () => {
this.setState({
name: 'Jane',
changed: true
});
};
render() {
console.log('render called');
return (
<div>
<h1>Updating Example</h1>
<div>Name {
this.state.changed ?
<h3>{this.state.name}</h3> :
<p>{this.state.name}</p>}
</div>
<button onClick={this.changeName}>Change Name</button>
</div>
);
}
}
export default UpdatingExample;
불필요한 컴포넌트 리렌더링을 방지하는데 사용된다. 기본적으로 props와 state가 변경될 때 컴포넌트가 리렌더링되는데, true 또는 false를 반환해서 조건에 따라 컴포넌트를 리렌더링 할지 말지 결정할 수 있다.
nextProps와 nextState를 인자로 받아서 현재 props와 state와 비교할 수 있다.
render() 직전에 호출된다. DOM이 업데이트되기 전의 상태를 기록할 수 있다. 주로 DOM의 스크롤 위치, 크기 변경 등의 정보를 기록할 때 유용하다. 반환된 값은 componentDidMount()
의 세번 째 인자로 전달된다.
render() 이후에 호출된다. 주로 DOM 트리를 동작하거나, 전달된 인자중 하나가 변경될 때 발생하는 사이드 이펙트를 처리하는데 사용된다. useEffect에서 의존성 배열 내부의 데이터가 변경될 때 로직이 실행되는 것을 생각하면 편할 듯하다.
컴포넌트가 DOM에서 제거되고, 더 이상 렌더링되지 않거나 접근할 수 없을 때 발생한다. 이 단계에서 리액트를 컴포넌트와 관련된 리소스를 DOM 트리에서 제대로 정리하기 위해 몇가지 작업을 수행한다.
import React from "react";
class Child extends React.Component {
componentDidMount() {
console.log('Component mounted');
}
componentWillUnmount() {
console.log('Component unmounted');
}
render() {
return (
<div>
<p>Child Component content</p>
</div>
);
}
}
export default class UnmountingExample extends React.Component {
constructor(props) {
super(props);
this.state = {
showComponent: true
};
}
toggleComponent = () => {
this.setState(prevState => ({
showComponent: !prevState.showComponent
}));
};
render() {
return (
<div>
<h1>Main Component</h1>
{this.state.showComponent && <Child />}
<button onClick={this.toggleComponent}>
{this.state.showComponent ? 'Unmount' : 'Mount'}
</button>
</div>
);
}
}
컴포넌트가 DOM에서 제거되기 직전에 호출된다. useEffect의 return문을 생각하면 될 듯하다. 그와 마찬가지로 타이머 취소, 이벤트 리스너 제거, 데이터 구조 정리 등 메모리 누수를 방지하기 위한 작업을 하는데 사용된다. 또한 컴포넌트의 모든 상태와 props는 소멸된다.
https://medium.com/@arpitparekh54/understanding-the-react-component-lifecycle-a-deep-dive-into-the-life-of-a-react-component-74813cb8dfb5
https://massivepixel.io/blog/react-lifecycle-methods/
https://levelup.gitconnected.com/react-lifecycle-methods-and-their-equivalents-in-functional-components-5677a3fa623d