Effect Hook을 사용하면 함수 컴포넌트에서 side effect를 수행할 수 있다.
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// componentDidMount, componentDidUpdate와 같은 방식으로
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>
);
}
데이터 가져오기, 구독(subscription) 설정하기, 수동으로 리액트 컴포넌트의 DOM을 수정하는 것까지 모든 것이 side effects이다. 이런 기능들(operations)을 side effect(혹은 effect)라 부르는 것이 익숙하지 않을 수도 있다.
TIP
리액트의 class 생명주기 메서드에 친숙하다면, useEffect Hook을 componentDidMount와 componentDidUpdate, componentWillUnmount가 합쳐진 것으로 생각해도 좋다.
리액트 컴포넌트에는 일반적으로 두 종류의 side effects가 있다.
리액트가 DOM을 업데이트한 뒤 추가로 코드를 실행해야 하는 경우가 있다. 네트워크 리퀘스트, DOM 수동조작, 로깅 등은 정리(clean-up)가 필요 없는 경우들이다. 이러한 예들은 실행 이후 신경 쓸 것이 없기 때문이다. class와 hook이 이러한 side effects를 어떻게 다르게 구현할지 살펴봐야 한다.
useEffect Hook을 이용해 우리는 리액트에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야하는 지를 말한다. 리액트는 우리가 넘긴 함수를 기억했다가(이 함수를 'effect'라고 부른다) DOM업데이트를 수행한 이후에 불러낼 것이다. 문서 타이틀을 지정하거나, 이외에도 데이터를 가져오거나 다른 명령형 API를 불러내는 일도 할 수 있다.
useEffect를 컴포넌트 내부에 둠으로써 effect를 통해 count state변수(또는 그 어떤 prop에도)에 접근할 수 있게 된다. 함수 범위 안에 존재하기 때문에 특별한 API 없이도 값을 얻을 수 있다. Hook은 자바스크립트의 클로저를 이용해 리액트에 한정된 API를 고안하는 것보다 자바스크립트가 이미 가지고 있는 방법을 이용해 문제를 해결한다.
기본적으로 첫번째 렌더링과 이후의 모든 업데이트에서 수행된다. 마운팅과 업데이트라는 방식으로 생각하는 대신 effect를 렌더링 이후에 발생하는 것으로 생각하는 것이 더 수월하다. 리액트는 effect가 수행되는 시점에 이미 DOM이 업데이트 되었음을 보장한다.
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
}
count state 변수를 선언한 뒤 리액트에게 effect를 사용함을 말하고 있다. useEffect Hook에 함수를 전달하고 있는데, 이 함수가 바로 effect이다. 이 effect 내부에서 document.title이라는 브라우저 AIP를 이용해 문서 타이틀을 지정한다. 같은 함수 내부에 있기 때문에 최신의 count를 바로 얻을 수 있다.
컴포넌트를 렌더링할때 리액트는 우리가 이용한 effect를 기억했다가 DOM을 업데이트한 이후에 실행한다.
TIP
componentDidMount 혹은 componentDidUpdate와는 달리 useEffect에서 사용되는 effect는 브라우저가 화면을 업데이트하는 것을 차단하지 않는다. 이를 통해 애플리케이션의 반응성을 향상시켜준다.
위에서 정리(clean-up)가 필요하지 않은 side effect를 보았지만, 정리(clean-up)가 필요한 effect도 있다. 외부 데이터에 구독(subscription)을 설정해야 하는 경우를 생각해보자. 이런 경우에 메모리 누수가 발생하지 않도록 정리(clean-up)하는 것은 매우 중요하다.
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, hadleStatusChange);
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handelStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
이는 effect를 위한 추가적인 정리 매커니즘이다. 모든 effect는 정리를 위한 함수를 반환할 수 있다. 구독(subscription)의 추가와 제거가 모두 하나의 effect를 구성하는 것이다.
리액트는 컴포넌트가 마운트 해체되는 때에 정리(clean-up)을 실행한다. 하지만 위의 예시에서 보았듯이 effect는 한번이 아니라 렌더링이 실행되는 때마다 실행된다. 리액트가 다음 차례의 effect를 실행하기 전에 이전의 렌더링에서 파생된 effect 또한 정리하는 이유가 바로 이 때문이다.
useEffect가 컴포넌트의 렌더링 이후에 다양한 side effects를 표현할 수 있음을 위에서 배웠다. effect에 정리(clean-up)가 필요한 경우에는 함수를 반환한다.
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
정리 (clean-up)가 필요없는 경우에는 어떤 것도 반환하지 않는다.
useEffect(() => {
document.title = `You clicked ${count} times`;
});