React Lifecycle
모든 리액트 컴포넌트는 3단계의 생애주기를 갖는다.
Mounting
: inserting elements into the DOMUpdating
: involves methods for updating components in the DOMUnmounting
: removing a component from the DOM모든 단계는 각 메소드를 가지며, 메소드를 사용해서 컴포넌트에 특정 작업을 수행할 수 있다. 클래스 컴포넌트의 경우 React.Component에서 extend하여 메서드를 사용한다.
Class components vs functional components
클래스 컴포넌트에서 생애주기 메소드를 사용했다면, 함수형 컴포넌트에서는 useEffect hook을 사용할 수 있다. 2019년 3월 리액트 16.8이 출시되며 함수형 컴포넌트에서 useState와 useEffect 등의 hook을 사용해 상태와 생애주기 메소드를 구현할 수 있게되었다.
먼저, 클래스 컴포넌트에서는 어떠한 주요 생애주기 메소드를 사용했는지 살펴보도록 하자.
Class components Lifecycle methods
componentDidMount
componentDidMount() {
fetch(url).then(res => {
// Handle response in the way you want.
// Most often with editing state values.
})
}
componentDidUpdate
componentWillUnmount
Function components useEffect
useEffect는 함수 컴포넌트에서 side effects가 발생하도록 하는데, 여기서 side effects란, 데이터 가져오기(data fetching), 구독설정(setting up a subscription), DOM 변경(manually changing the DOM) 등을 말한다.
useEffect는 명령형 또는 effect를 발생시키는 함수를 인자로 받는다. useEffect에 전달된 함수는 기본적으로 화면에 렌더링이 완료된 후에 수행되지만, 어떤 값이 변경되었을때만 실행되게 할 수도 있다.
쉽게 말하면, 위에서 확인한 클래스 컴포넌트의 3가지 생애주기 메소드들이 합쳐진 것이 useEffect라고 할 수 있다.
useEffect는 두번째 인자로 무엇을 적어주는지에 따라 effect가 실행되는 경우를 제어할 수 있다.
없는 경우 useEffect (()=> effect )
컴포넌트가 렌더링 될때마다 effect가 호출된다.
빈배열인 경우 useEffect (()=> effect, [])
컴포넌트가 처음 마운트되어 렌더링되었을때에만 effect가 호출된다.
배열의 요소가 있는 경우 useEffect (()=> effect, [value])
특정값이 업데이트될때에만 effect가 호출된다.
다음으로 side effect에는 cleanup이 필요한 것과 필요하지 않은 것들이 있는데, 각각을 살펴보도록 하자.
DOM이 업데이트 된 후에 네트워크 요청을 보내거나, DOM을 변형하거나 로깅을 하는 등의 side effect는 cleanup이 필요하지 않다.
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
setInterval, setTimeout을 사용해 등록한 작업이나 effect에서 생성한 이벤트 리스너가 중복 생성되지 않도록 제거하거나, 구독을 취소하는 경우 필요하다.
cleanup 함수는 함수를 리턴하는 형태로 작성할 수 있으며, 컴포넌트가 unmount될때 실행되어 순서를 생각해보면 컴포넌트 unmount -> cleanup 함수 실행 -> 컴포넌트 mount -> effect 실행
으로 설명할 수 있다.
React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time. (공식문서)
🙋 다음 예시에서는 버튼을 누르면,
resourceType
이 바뀌고,<h1>
태그에 resourceType
이 나타난다. (렌더링 먼저)import React, { useState, useEffect } from 'react';
function Example() {
const [resourceType, setResourceType] = useState('posts');
useEffect(() => {
console.log('resource changed');
return () => {
console.log('clean up function working');
}
},[resourcetype]);
return (
<>
<div>
<button onClick={ () => setResourceType('posts') }>Posts</button>
<button onClick={ () => setResourceType('users') }>Users</button>
<button onClick={ () => setResourceType('comments') }>Comments</button>
</div>
<h1>{resourceType}</h1>
</>
)
}
🙋 useEffect 내에 cleanup 함수를 작성하지 않았을때, 에러가 발생할 수 있는 경우는 다음과 같다.
Home 컴포넌트에서 useEffect로 fetch 요청을 보내서 상태를 업데이트한다.
New Blog 버튼을 클릭해 Home 컴포넌트가 unmount되고, New Blog 컴포넌트가 mount된다.
fetch 요청을 마친 뒤 상태를 업데이트하려고 했으나, 이미 Home 컴포넌트는 unmount되어 상태를 업데이트할 수 없다.
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function)
과 같은 에러가 발생한다.
👉 unmount된 컴포넌트의 상태를 업데이트 할 수 없고, 현재 애플리케이션에서 메모리가 누수되고 있으므로 useEffect에서 호출하는 구독과 비동기 요청을 cleanup function으로 모두 취소시키라는 에러이다.
⭐ 이 에러는 fetch 요청을 취소하는 cleanup 함수를 통해 해결할 수 있으며, 이때, AbortController라는 웹 요청을 취소할 수 있는 객체 인터페이스를 사용한다. (axios 요청에서는 AbortController와 동일한 기능을 하는 cancelToken을 생성해 CancelToken.source 메소드를 사용한다. )
new AbortController()
로 새로운 객체 인터페이스 생성//fetch
useEffect( () => {
const abortCont = new AbortController(); // AbortController 생성
setTimeout( ()=>{
fetch( url, {signal: abortCont.signal} )
.then( res => {
if(!res.ok) throw Error('could not fetch');
return res.json();
})
.then( data => {
setData(data);
})
.catch( err => {
if(err.name === 'AbortError'){
console.log('fetch aborted');
} else{
setError(err.message);
}
})
}, 1000 )
return () => abortCont.abort(); // DOM 요청이 완료되기 전에 취소
},[url] )
reference