Effect Hook은 함수 컴포넌트에서 side effect를 수행하기 위해 만들어졌다. React 컴포넌트가 화면에 렌더링된 이후에 비동기로 처리되어야 하는 부수적인 효과들을 흔히 side effect라고 일컫는다. 대표적인 예시로 외부 api를 호출하는 경우, 일단 화면에 렌더링할 것은 렌더링하고 side effect로 데이터는 비동기로 가져오는 형식으로 사용한다. 요청 즉시 렌더링은 진행함으로써 api 응답인 늦어지거나 없는 경우에도 영향을 최소화할 수 있어 사용자 경험 측면에서 유리하기 때문이다.
React에서는 side effect를 다음과 같이 설명했다.
데이터 가져오기, 구독(subscription) 설정하기, 수동으로 React 컴포넌트의 DOM을 수정하는 것까지 이 모든 것이 side effects입니다.
useEffect(callback, deps)의 형태로 effect Hook을 사용할 수 있다. 아래 예시를 통해 useEffect의 기본 동작 원리를 이해해보자.
useState에 대한 내용은 아래 링크에서 확인할 수 있다.
[React Hooks] useState
import React, { useState, useEffect } from "react";
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
// 브라우저 API를 이용하여 문서 타이틀을 업데이트합니다.
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
해당 코드로 작성된 웹페이지에서 버튼을 클릭할 때마다 title에 있는 count가 1씩 증가한다. 이 과정을 세부적으로 뜯어보면, 아래와 같은 원리로 작동하는 것이다.
_리액트
state가 0일때 UI 보여줘. 버튼을 클릭하면 count state가 1씩 증가할거야.
_컴포넌트
<p>You clicked 0 times</p>
이거 렌더링 해주면 돼.
렌더링 끝나면 () => { document.title = 'You clicked 0 times'; } 이 함수도 같이 실행해줘.
_리액트
OK 알겠어. 브라우저야 이거 DOM에 반영해줘.
_브라우저
반영했어.
_리액트
렌더링이 끝났으니까 컴포넌트가 부탁한 콜백함수 실행시켜줄게.
위와 같은 과정이 state가 업데이트될 때마다 반복된다. 이를 아래와 같이 요약할 수 있다.
렌더링 -> effect 실행 -> state 업데이트 -> 리렌더링 -> effect 실행 -> ...
먼저 아래 예시를 통해 deps 배열이 필요한 이유에 대해 알아보자.
function App() {
const [count, setCount] = useState(0);
const [countCleanup, setCountCleanup] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
console.log(count);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount((prev) => prev + 1)}>
Click me
</button>
<p>You clicked {countCleanup} times</p>
<button onClick={() => setCountCleanup((prev) => prev + 1)}>
Click me for Clean-up
</button>
</div>
);
}
위 코드에서 두번째 'Click me for Clean-up' 버튼을 클릭한다면 countCleanup state가 업데이트 되기 때문에 리렌더링이 일어난다. 또한, 리렌더링이 일어났기 때문에 count가 업데이트 되지 않았음에도, effect가 실행된다(console.log(count)로 확인이 가능하다.). Effect와 관련이 있는 count state가 업데이트될 때만 effect가 실행되는 것이 이상적인데, 계속 불필요하게 effect가 실행되고 있는 것이다. 이러한 문제를 해결해주는 것이 deps 배열이다. useEffect 함수를 아래와 같이 수정해보자.
useEffect(() => {
document.title = `You clicked ${count} times`;
console.log(count);
}, [count]);
이제 count가 업데이트될 때만 effect가 실행되어 불필요한 effect 실행 문제를 해결할 수 있다. 일반적으로 이펙트 내부에서 사용되는 모든 변수는 deps에 명시를 하는 것이 권장된다. 그렇지 않으면 버그가 발생할 가능성이 있기 때문이다. 만약 deps에 빈 배열 []을 넣는다면, effect가 첫 렌더링에 딱 한번만 실행된다.
Clean-up function은 effect가 함수를 반환하면 그 함수를 정리할 필요가 있을 때 사용된다. Clean-up function은 콜백 함수의 return 값으로 작성해주면 된다. 우선 동작 원리를 이해하기 위해 아래 예시를 살펴보자.
function App() {
const [count, setCount] = useState(0);
const [countCleanup, setCountCleanup] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
return () => setCountCleanup((prev) => prev + 2);
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount((prev) => prev + 1)}>
Click me
</button>
<p>Cleanup Count: {countCleanup}</p>
</div>
);
}
해당 웹페이지에서 버튼을 클릭할 때마다 count는 1씩 증가하고, countCleanup은 2씩 증가하는 것을 확인할 수 있다. 이 과정을 세부적으로 뜯어보면, 아래와 같은 원리로 작동하는 것이다.
즉, 컴포넌트가 리렌더링된 후에 clean-up 함수, 새로운 effect가 순서대로 실행되는 것이다. 보통 clean-up 함수는 메모리를 효율적으로 사용하기 위해 콜백함수에서 생성한 함수를 정리해줘야 할 때 사용한다.