useEffect는 React 함수형 컴포넌트에서 side effects를 수행하기 위해 사용되는 Hook이다. 컴포넌트가 렌더링된 이후에 비동기적으로 실행되며, 여러 개의 useEffect를 사용하여 다양한 side effects를 처리할 수 있다.
데이터 가져오기, 구독(subscription) 설정하기, 수동으로 React 컴포넌트의 DOM을 수정하는 것까지 이 모든 것이 side effects다.
React 컴포넌트에는 일반적으로 두 종류의 side effects가 있다. 정리(clean-up)가 필요한 것과 그렇지 않은 것. 이 둘을 어떻게 구분해야 할지 자세하게 알아보자.
React가 DOM을 업데이트한 뒤 추가로 코드를 실행해야 하는 경우가 있다.
네트워크 request, DOM 수동 조작, 로깅 등은 정리(clean-up)가 필요 없는 경우들이다. 이러한 예들은 실행 이후 신경 쓸 것이 없기 때문이다. class와 hook이 이러한 side effects를 어떻게 다르게 구현하는지 비교해보자.
React의 class 컴포넌트에서 render 메서드 그 자체는 side effect를 발생시키지 않는다. 이때는 아직 이른 시기로서 이러한 effect를 수행하는 것은 React가 DOM을 업데이트하고 난 이후다.
React class에서 side effect를 componentDidMount
와 componentDidUpdate
에 두는 것이 바로 이 때문이다. 예시로 돌아와서 React가 DOM을 바꾸고 난 뒤 문서 타이틀을 업데이트하는 React counter
클래스 컴포넌트를 보자.
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
위 코드에서 class
안에 두 개의 생명주기 메서드에 같은 코드가 중복된다.
이는 컴포넌트가 막 마운트된 단계인지 아니면 업데이트되는 것인지에 상관 없이 side effect를 수행해야 하기 때문이다. 개념적으로 렌더링 이후에는 항상 같은 코드가 수행되기를 바라는 것이다. 하지만 React 클래스 컴포넌트는 그런 메서드를 가지고 있지 않다. 함수를 별개의 메서드로 뽑아낸다고 해도 여전히 두 장소에서 함수를 불러내야 한다.
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>
);
}
useEffect
Hook을 이용하여 React에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야하는 지를 말한다. React는 우리가 넘긴 함수를 기억했다가(이 함수를 effect
라고 부른다) DOM 업데이트를 수행한 이후에 불러낼 것이다. 위의 경우에는 effect를 통해 문서 타이틀을 지정하지만, 이 외에도 데이터를 가져오거나 다른 명령형(imperative) API를 불러내는 일도 할 수 있다.
useEffect
를 컴포넌트 내부에 둠으로써 effect를 통해 count state 변수(또는 그 어떤 prop에도)에 접근할 수 있게 된다. 함수 범위 안에 존재하기 때문에 특별한 API 없이도 값을 얻을 수 있는 것이다. Hook은 자바스크립트의 클로저를 이용하여 React에 한정된 API를 고안하는 것보다 자바스크립트가 이미 가지고 있는 방법을 이용하여 문제를 해결한다.
기본적으로 첫 번째 렌더링과 이후의 모든 업데이트에서 수행된다. 마운팅과 업데이트라는 방식으로 생각하는 대신 effect를 렌더링 이후에 발생하는 것으로 생각하는 것이 더 쉽다. React는 effect가 수행되는 시점에 이미 DOM이 업데이트 되었음을 보장한다.
위에서 정리(clean-up)가 필요하지 않은 side effect를 보았지만, 정리가 필요한 effect도 있다. 외부 데이터에 구독(subscription)을 설정해야 하는 경우를 생각해보겠다. 이런 경우에 메모리 누수가 발생하지 않도록 정리하는 것은 매우 중요하다. class와 Hook을 사용하는 두 경우를 비교해보겠다.
React class는 흔히 componentDidMount
에 구독을 설정한 뒤 componentWillUnmount
에서 이를 정리한다. 친구의 온라인 상태를 구독할 수 있는 ChatAPI 모듈의 예를 들어보자. 다음은 class를 이용하여 상태를 구독하고 보여주는 코드다.
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
componentDidMount
와 componentWillUnmount
가 어떻게 대칭을 이루고 있는지를 보자. 두 개의 메서드 내에 개념상 똑같은 effect에 대한 코드가 있음에도 불구하고 생명주기 메서드는 이를 분리하게 만든다.
이제 이 컴포넌트를 Hook을 이용하여 구현해보자.
정리의 실행을 위해 별개의 effect가 필요하다고 생각할 수 있지만, 구독의 추가와 제거를 위한 코드는 결합도가 높기 때문에 useEffect는 이를 함께 다루도록 고안되었다. effect가 함수를 반환하면 React는 그 함수를 정리가 필요한 때에 실행시킬 것이다.
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, handleStatusChange);
// effect 이후에 어떻게 정리(clean-up)할 것인지 표시합니다.
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
이는 effect를 위한 추가적인 정리 메커니즘이다. 모든 effect는 정리를 위한 함수를 반환할 수 있다. 이 점이 구독의 추가와 제거를 위한 로직을 가까이 묶어둘 수 있게 한다. 구독의 추가와 제거가 모두 하나의 effect를 구성하는 것이다.
React는 컴포넌트가 마운트 해제되는 때에 정리를 실행한다. 하지만 위의 예시에서 보았듯이 effect는 한번이 아니라 렌더링이 실행되는 때마다 실행된다.
React가 다음 차례의 effect를 실행하기 전에 이전의 렌더링에서 파생된 effect 또한 정리하는 이유가 바로 이 때문이다.
useEffect가 컴포넌트의 렌더링 이후에 다양한 side effects를 표현할 수 있음을 위에서 배웠다. effect에 정리가 필요한 경우에는 함수를 반환한다.
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
정리가 필요없는 경우에는 어떤 것도 반환하지 않는다.
useEffect(() => {
document.title = `You clicked ${count} times`;
});
이처럼 effect Hook은 두 가지 경우를 한 개의 API로 통합한다.
기존의 클래스 컴포넌트에서의 생명주기 메서드(componentDidMount, componentDidUpdate, componentWillUnmount 등)와 useEffect
의 관계를 알아보자.
componentDidMount: 클래스 컴포넌트에서 컴포넌트가 처음 마운트될 때 호출되는 메서드다. 이에 해당하는 역할을 하는 useEffect
는 두 번째 매개변수를 빈 배열 []
로 설정하여 컴포넌트가 처음 마운트될 때 실행될 수 있다.
componentDidUpdate: 클래스 컴포넌트에서 컴포넌트의 상태나 props가 변경될 때 호출되는 메서드다. useEffect
는 두 번째 매개변수로 종속성 배열 dependencies
를 활용하여 특정 상태나 props가 변경될 때만 실행되도록 할 수 있다.
componentWillUnmount: 클래스 컴포넌트에서 컴포넌트가 언마운트될 때 호출되는 메서드다. useEffect
는 clean-up 함수를 반환하는 형태로 이와 비슷한 역할을 수행할 수 있다. 이 clean-up 함수는 해당 useEffect
가 다시 실행되기 전에 호출된다.
기타 생명주기 메서드와 비슷한 역할을 하는 useEffect
를 사용하여 컴포넌트의 상태 변화나 화면 렌더링과 관련된 side effects를 처리할 수 있다. React Hooks를 활용하면 함수형 컴포넌트에서도 생명주기와 관련된 작업을 효과적으로 처리할 수 있다.