컴포넌트 생명주기의 세 단계
리액트 컴포넌트의 생명주기는 마운팅$_{mounting}$, 업데이트$_{update}$, 언마운팅$_{unmounting}$으로 크게 세 가지 주요 단계로 나눌 수 있다.
1. 마운팅 단계
마운팅 단계는 React 컴포넌트의 생명주기에서 컴포넌트가 생성되는 단계이다. 최초로 DOM에 삽입되며 애플리케이션에서 컴포넌트가 시작된다. 이 과정에서 몇 가지 핵심적인 생명주기 메서드가 호출되며 각각의 메서드는 컴포넌트의 초기화와 설정에 필수적인 역할을 한다.
constructor(props)
constructor 메서드는 컴포넌트의 생성자 함수로, 컴포넌트가 생성될 때 가장 먼저 실행된다. 이 메서드는 컴포넌트의 초기 상태를 설정하거나, 이벤트 핸들러를 바인딩하는 데 주로 사용된다.
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = { greeting: 'Hello' };
}
render() {
return <h1>{this.state.greeting}, World!</h1>;
}
}
static getDerivedStateFromProps(props, state)
getDerivedStateFromProps 메서드는 컴포넌트가 새로운 props를 받거나 리렌더링될 때마다 호출된다.
이 메서드를 통해 상태를 업데이트할 수 있으며 state의 변경이 props의 변경에 의존할 때 유용하게 사용된다.
class Welcome extends React.Component {
state = { greeting: 'Hello' };
static getDerivedStateFromProps(props, state) {
if (props.newGreeting !== state.greeting) {
return { greeting: props.newGreeting };
}
return null;
}
render() {
return <h1>{this.state.greeting}, World!</h1>;
}
}
render()
render 메서드는 컴포넌트의 필수 메서드로, 컴포넌트가 어떻게 보여질지 결정한다. this.props와 this.state에 기반하여 JSX나 null을 반환한다.
React는 이 메서드의 반환값을 사용하여 DOM을 업데이트한다.
class Welcome extends React.Component {
state = { greeting: 'Hello' };
render() {
return <h1>{this.state.greeting}, World!</h1>;
}
}
componentDidMount()
componentDidMount 메서드는 컴포넌트가 마운트된 직후, 즉 DOM에 삽입된 직후에 호출된다.
이 시점에서 네트워크 요청을 보내거나, 이벤트 리스너를 추가하는 등의 작업을 수행할 수 있다.
컴포넌트가 화면에 나타난 후 필요한 데이터를 불러오거나, 외부 라이브러리를 초기화하는 데 자주 사용된다.
class Welcome extends React.Component {
state = { greeting: 'Hello' };
componentDidMount() {
setTimeout(() => {
this.setState({ greeting: 'Hello, World!' });
}, 3000);
}
render() {
return <h1>{this.state.greeting}</h1>;
}
}
2. 업데이트 단계
업데이트 단계는 컴포넌트의 props나 state 변경으로 인해 재렌더링이 발생할 때 일어나는 리액트 컴포넌트 생명주기의 중요한 부분이다. 사용자 인터페이스가 애플리케이션의 가장 최신 데이터와 상태를 반영하도록 보장하는 데 필수적이다.
리액트 클래스 컴포넌트는 업데이트를 효율적으로 처리하기 위한 여러 생명주기 메서드를 제공하여 개발자가 업데이트 과정을 제어하고 부작용을 수행하며 성능을 최적화할 수 있게 한다.
static getDerivedStateFromProps(props, state)
이 정적 메서드는 초기 마운트와 이후 업데이트 모두에서 render 메서드 바로 전에 호출된다. 컴포넌트가 props의 변경 결과로 내부 상태를 업데이트할 수 있게 한다.
class UserProfile extends React.Component {
static getDerivedStateFromProps(props, state) {
// props 변경에 따라 상태 업데이트
if (props.userID !== state.lastUserID) {
return {
lastUserID: props.userID,
userData: null // 새 사용자 데이터를 위한 Placeholder
};
}
// 상태 변경이 없음을 나타내기 위해 null 반환
return null;
}
}
shouldComponentUpdate(nextProps, nextState)
이 메서드는 다음 props와 state가 업데이트를 트리거하지 않아야 한다면 false를 반환함으로써 불필요한 재렌더링을 피할 수 있게 해준다. 이는 side effect를 위해 사용되지 않는 성능 최적화 방법이다.
class UserProfile extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// userID가 변경된 경우에만 업데이트
return nextProps.userID !== this.props.userID;
}
}
render()
이 단계에서 render 메서드는 다시 호출된다. this.props와 this.state를 검사하고 업데이트된 JSX를 반환한다.
이 메서드는 순수해야 한다. 컴포넌트 상태를 수정하거나 부작용을 반환하거나 브라우저와 직접 상호작용해서는 안 된다.
getSnapshotBeforeUpdate(prevProps, prevState)
가장 최근에 렌더링된 출력이 DOM에 커밋되기 바로 전에 이 메서드가 호출된다. 업데이트로 인해 변경될 수 있는 DOM에서 정보(예: 스크롤 위치)를 캡처할 수 있게 해준다.
class UserList extends React.Component {
getSnapshotBeforeUpdate(prevProps, prevState) {
if (prevProps.list.length < this.props.list.length) {
const list = document.getElementById('userList');
return list.scrollHeight - list.scrollTop;
}
return null;
}
}
componentDidUpdate(prevProps, prevState, snapshot)
업데이트가 발생한 직후에 이 메서드가 호출된다. prop 또는 state 변경에 응답하여 DOM 업데이트나 데이터 가져오기와 같은 작업에 유용하다.
snapshot 매개변수는 getSnapshotBeforeUpdate에서 반환된 값을 포함한다.
class UserProfile extends React.Component {
componentDidUpdate(prevProps) {
if (this.props.userID !== prevProps.userID) {
this.fetchUserData(this.props.userID);
}
}
fetchUserData(userID) {
// 이 메서드가 사용자 데이터를 가져와 상태를 업데이트한다고 가정
}
}
예제 - UserProfile 컴포넌트
생명주기 메서드를 결합하여 UserProfile 컴포넌트는 prop 변경에 기반한 내용을 효율적으로 업데이트하고 불필요한 re-rendering을 방지할 수 있다.
class UserProfile extends React.Component {
state = { userData: null, lastUserID: null };
static getDerivedStateFromProps(props, state) {
if (props.userID !== state.lastUserID) {
return { lastUserID: props.userID };
}
return null;
}
shouldComponentUpdate(nextProps, nextState) {
return nextProps.userID !== this.props.userID;
}
componentDidUpdate(prevProps) {
if (this.props.userID !== prevProps.userID) {
this.fetchUserData(this.props.userID);
}
}
fetchUserData(userID) {
fetch(`/api/users/${userID}`)
.then(response => response.json())
.then(data => this.setState({ userData: data }));
}
render() {
const { userData } = this.state;
return (
<div>
{userData ? (
<div>
<h1>{userData.name}</h1>
<p>{userData.bio}</p>
</div>
) : (
<p>사용자 데이터를 불러오는 중...</p>
)}
</div>
);
}
}
이 예시에서 UserProfile은 userID props의 변경에 효과적으로 반응하고 필요에 따라 새로운 데이터를 가져오며 불필요한 재렌더링을 방지하여 리액트의 컴포넌트 생명주기 메서드의 힘과 유연성을 업데이트 단계에서 보여준다.
3. 언마운팅 단계
리액트 컴포넌트 생명주기의 마지막 단계인 언마운팅 단계는 간과하기 쉽다. 하지만 리소스 효율성을 보장하고 메모리 누수를 방지하는 데 중요한 역할을 한다.
컴포넌트의 생명주기가 끝나고 DOM에서 제거될 준비를 할 때 리액트는 필요한 정리 작업을 실행하기 위해 componentWillUnmount라는 생명주기 메서드를 제공한다.
이 단계는 컴포넌트의 생애 동안 설정된 작업들, 예를 들어 타이머, 네트워크 요청, 이벤트 리스너 등이 제대로 해제되도록 보장한다.
componentWillUnmount 이해하기
componentWillUnmount 메서드는 컴포넌트가 DOM에서 제거되기 바로 직전에 호출된다.
이 메서드는 컴포넌트가 자신이 사용한 것들을 깔끔하게 정리하여 성능에 영향을 줄 수 있는 원치 않는 잔여물을 남기지 않도록 한다.
componentWillUnmount에서의 주요 작업
- 네트워크 요청 취소: 언마운트된 컴포넌트에서 상태 설정을 방지하여 메모리 누수를 야기할 수 있다.
- 타이머와 인터벌 클리어: 컴포넌트가 제거된 후에도 타이머가 계속 실행되지 않도록 보장한다.
- 이벤트 리스너 제거: 컴포넌트의 생명 동안 추가된 전역이나 문서 수준의 이벤트 리스너를 정리한다.
타이머 컴포넌트 예제
다음 예제는 마운트할 때 인터벌을 설정하고 언마운트할 때 이 인터벌을 클리어하는 간단한 타이머 컴포넌트를 보여준다. 이는 언마운팅 단계에서 정리의 중요성을 보여준다.
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = { time: 0 };
this.tick = this.tick.bind(this);
}
componentDidMount() {
this.interval = setInterval(this.tick, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
tick() {
this.setState((prevState) => ({ time: prevState.time + 1 }));
}
render() {
return <div>Time elapsed: {this.state.time} seconds</div>;
}
}
이 Timer 컴포넌트에서 componentDidMount 메서드는 매 초마다 time state를 증가시키는 인터벌을 설정하는 데 사용된다.
componentWillUnmount 메서드는 이 인터벌을 클리어하여 언마운트된 컴포넌트의 상태를 업데이트하려는 인터벌 콜백이 무효가 되는 것을 방지한다. 이는 비효율적일 뿐만 아니라 오류를 일으킬 수도 있다.
Cleanup의 중요성
언마운팅 단계에서의 Cleanup 과정은 여러 가지 이유로 중요하다.
- 성능: 타이머가 계속 실행되거나 이벤트 리스너가 연결된 채로 남아 있으면 불필요한 리소스 소비로 이어져 애플리케이션을 느리게 할 수 있다.
- 메모리 누수: 정리를 하지 않으면 애플리케이션이 더 이상 필요하지 않은 메모리를 계속해서 잡고 있게 되어 다른 프로세스에 사용할 수 있는 메모리가 줄어들고, 잠재적으로 충돌$_{potenentially \space crashing}$을 일으킬 수 있다.
- 예상치 못한 동작: 언마운트된 컴포넌트에서 상태 업데이트와 같은 작업을 계속 수행하면 애플리케이션 동작이 예측 불가능해지고 디버그하기 어려운 오류를 일으킬 수 있다.
함수형 컴포넌트와 훅
리액트 생태계는 16.8 버전에서 훅(Hooks)이 도입되면서 크게 발전했고 컴포넌트가 정의되는 방식과 side effects가 처리되는 방식에서 패러다임의 변화되었다. 사실상 위의 내용들은 이 이후로 legacy가 되었다.
https://react.dev/reference/react/legacy
특히 함수형 컴포넌트의 기능을 크게 향상시켜 전통적으로 클래스 컴포넌트의 전유물이었던 생명주기 이벤트와 부수 효과를 효과적으로 관리할 수 있게 해준다. 더 자세한 이야기는 다음 포스팅에서 진행할 것이다.
https://laurent.tistory.com/entry/React-useEffect-%ED%9B%85
references
https://react.dev/learn/state-a-components-memory
https://react.dev/learn/synchronizing-with-effects
'Web, Front-end' 카테고리의 다른 글
[React] 하나의 index.ts에서 import 및 export 하기 (0) | 2024.05.09 |
---|---|
[React] 컴포넌트 분리의 중요성 (0) | 2024.02.22 |
[React] 프로젝트 폴더(디렉토리) 구조 (0) | 2024.02.18 |
[React] 컴포넌트 간 데이터 전달을 위한 props의 이해 (0) | 2024.02.17 |
[Javascript] 웹 브라우저 환경에서의 자바스크립트 (0) | 2024.02.14 |
컴퓨터 전공 관련, 프론트엔드 개발 지식들을 공유합니다. React, Javascript를 다룰 줄 알며 요즘에는 Typescript에도 관심이 생겨 공부하고 있습니다. 서로 소통하면서 프로젝트 하는 것을 즐기며 많은 대외활동으로 개발 능력과 소프트 스킬을 다듬어나가고 있습니다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!