리액트를 공부하면서 마주친 두번째 훅입니다. 사실 이게 저는 그렇게 이해가 안가더라구요. Side effect를 위해 있다지만, 그냥 함수 컴포넌트 안에서 구현하면 되는거 아닌가? 싶었습니다. 그리고 왜 side effect라고 부르는지도 모르겠구요. 그래서 여러 포스팅을 읽다보니, useEffect의 유래를 좀 알게되었고, dependency나 cleanup 이 왜 나왔는지도 조금 알게되었습니다. 이번 포스팅에서 useEffect와 그에 딸린 여러 개념들을 제가 이해한 선에서 최대한 정확하게 설명해 보겠습니다.
useEffect를 시작하기전에 알아두어야 할 개념이 하나 있습니다. 바로 클래스 컴포넌트입니다. 클래스 컴포넌트는 함수 컴포넌트처럼 컴포넌트를 만드는 방법중 하나인데요, 16.8 버전에서 훅이라는 개념이 등장하기 전까지 보편적으로 쓰이던 방법입니다. 그리고 이름에서 눈치챌 수 있듯, 정말 말그대로 클래스 모양을 하고 있습니다. State를 가질 수 있고, function을 내장할 수 있죠.
클래스 컴포넌트는 DOM 안에서 특정한 Life cycle을 가지고 있습니다. Dom에 올라갈때를 Mount, 그리고 유저가 변화를 주는 경우를 Update, 마지막으로 컴포넌트가 아예 사라질경우 Unmount라고 부르게 됩니다. 클래스 컴포넌트에서는 이 모든 phase마다 비동기적인 작업을 처리할 수 있게 알맞은 함수들을 가지고 있습니다. 사진 맨 아래의 세 함수가 그런 경우에 나오는 함수들인데요, 예를 들어 componentDidMount()는 컴포넌트가 첫 랜더시 필요한 비동기 작업을 실행하는 함수가 되겠습니다.
이렇게 클래스 컴포넌트에서는 비동기 처리를 함수를 통해 실행을 해주었는데요, 훅이 등장하고, 함수 컴포넌트로의 시프트가 일어나면서 저런 함수의 역할을 해줄 훅이 필요해졌습니다. 그 훅이 useEffect이고, dependency나 cleanup function을 통해서 각기 다른 life cycle에서의 비동기 작업을 수행합니다. 반대로, 어떤 life cycle phase에서 비동기 처리를 하고 싶은지 정확히 알때, dependency나 cleanup function을 정확하게 사용할 수 있겠지요. 지금부터 세가지 페이즈를 나눠서 어떻게 dep과 cleanup을 설정해 주는지 알아보겠습니다.
import { useEffect } from 'react';
function Greet({ name }) {
const message = `Hello, ${name}!`; // Calculates output
useEffect(() => {
// Good!
console.log('HI')
});
return <div>{message}</div>; // Calculates output
}
첫번째로 컴포넌트가 랜더 되었을때의 useEffect입니다. Greet 컴포넌트가 랜더 되었을때, useEffect를 통해서 HI를 출력하게 하였습니다. 가장 기본적인 형태의 useEffect이지만, name prop이 변할때마다 re-render가 일어날테고, 그때마다 useEffect가 실행되어서 HI를 출력할겁니다. 그런걸 원하지 않기때문에, 특정 조건에서 useEffect가 일어나게끔 dependency를 설정해줘야 하는겁니다.
import { useEffect } from 'react';
function Greet({ name }) {
const message = `Hello, ${name}!`; // Calculates output
useEffect(() => {
// Good!
console.log('HI')
}, []);
return <div>{message}</div>; // Calculates output
}
dep에 아무것도 들어있지 않으면, 최초 마운트시에 실행되고 끝납니다.
dependency는 state의 배열로, 그 중 하나라도 변하면 useEffect를 실행하게끔 합니다. 따라서 state가 변하고 컴포넌트가 리랜더가 되었을때, dep에 변하는 state가 있다면, useEffect를 실행합니다.
import { useEffect } from 'react';
function Greet({ name }) {
const message = `Hello, ${name}!`; // Calculates output
useEffect(() => {
// Good!
document.title = `Greetings to ${name}`; // Side-effect!
}, [name]);
return <div>{message}</div>; // Calculates output
}
위의 예시에서 name이 바뀌는 경우, 컴포넌트 리랜더가 일어나게 되고, 그에따라 useEffect가 실행되게끔 만들어져있습니다.
마지막으로 컴포넌트가 DOM에서 내려오는 경우입니다. 리액트는 컴포넌트가 언마운트될때 작동중인 effect들을 종료시킬 수 있는 cleanup function을 제공하고 있습니다. 하지만, 컴포넌트가 새로 랜더 될때 마다 useEffect가 실행이 되기 때문에, 그 전에 실행되는 effect들도 cleanup으로 정리할수 있게 됩니다. 결국 매 랜더마다 cleanup을 한다는 얘기가 되네요.
const [windowSize, setWindowSize] = useState(0);
useEffect(()=>{
window.addEventListener("resize",windowSizeHandler)
},[])
const windowSizeHandler = () =>{
setWindowSize(window.innerwidth)
}
return ...
이해를 돕기위해 한 컴포넌트의 일부를 가져왔습니다. windowSize라는 state가 창 크기값을 담고있고, 창의 크기가 바뀔때마다 값이 저장되게 만들었습니다. 위 코드의 문제는, 유저가 창을 바꿀때마다 eventListener가 매번 새로 생기면서 리소스를 잡아먹는다는 겁니다. 이런 문제를 방지하기 위해서, cleanup function으로 useEffect 가 trigger될때마다 기존의 eventListener를 제거하고 새로운 listener달아주면 됩니다.
const [windowSize, setWindowSize] = useState(0);
useEffect(()=>{
window.addEventListener("resize",windowSizeHandler)
return () =>{
window.removeEventListener("resize",windowSizeHandler)
}
},[])
const windowSizeHandler = () =>{
setWindowSize(window.innerwidth)
}
return ...
이렇게 cleanup을 달아주면, 새로운 effect가 실행되기 전에 listener를 지우고 새로운 listener를 달게 되는 겁니다.
이해를 위해 단순한 예시를 들었지만, 주로 fetch request에 응답은 아직 없고, 새 request를 받아야 하는 상황에서, 기존의 request를 취소하는데 cleanup이 사용되기도 합니다.
이번엔 useEffect에 대해 알아보았습니다. useEffect란것이 async function을 다룰때 사용하려 만들어 졌지만, 크게 세 종류의 페이즈를 고려하고 디자인이 되었다는 것과, deps와 cleanup이 어떻게 부수적인 역할을 하는지도 알게되었습니다.
사실 저도 공부하는 입장이라 제 글이 얼마나 정확한지, 얼마나 심도있는지는 모르겠습니다ㅎㅎ 그냥 이렇구나! 하고 넘어간 부분도 있고, 아직 제가 캐치하지 못한 부분도 있을것 같습니다. 다음에는 아마도 VDOM에 대해 포스팅을 할거 같아요. 감사합니다!
https://dmitripavlutin.com/react-useeffect-explanation/
https://dmitripavlutin.com/react-cleanup-async-effects/
https://blog.logrocket.com/understanding-react-useeffect-cleanup-function/
https://www.youtube.com/watch?v=UVhIMwHDS7k&t=1276s