useEffect 알아보기
Effect Hook를 사용하면 함수 컴포넌트에서 side effect를 수행할 수 있다.
* side effect 혹은 effect는 React 컴포넌트 안에서 데이터를 가져오거나 구독하고 DOM을 직접 조작하는 작업을 말한다.
리액트의 class 생명주기 메소드 중 comonentDidMount 와 componentDidUpdate, 그리고 comonentWillUnmount, 이 세개가 합쳐진 것이 useEffect다 라고 생각해도 좋다.
리액트 컴포넌트는 일반적으로 두 종류의 side effects 가 있다.
- clean-up 이 필요한것
- 그렇지 않은것
Clean-up을 이용하지 않는 Effects
- DOM을 업데이트 한 뒤 추가로 코드를 실행해야하는 경우 ( 네트워크 요청, DOM 수동 조작 등은 실행 이후 신경 쓸 것이 없기 때문에 clean-up 이 필요 없는 경우)
Class 에서의 side effect
- ComponentDidMount와 componentDidUpdate를 두는것이 위와 같은 이유이다.
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>
);
}
}
// 리액트가 DOM을 바꾸고 난 뒤 문서 타이틀을 업데이트
// 하는 리액트 counter 클래스 컴포넌트
- 컴포넌트가 막 마운트 된 단계인지 업데이트 되는 것인지 상관없이 같은 side effect를 수행하기 때문에 중복 코드로 만들었다.
Hook를 이용하는 예시
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>
);
}
useEffect가 하는 일
- 리액트에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야하는지를 말한다.
- 리액트는 우리가 넘긴 함수를 기억했다가 ("effect" 를 기억했다가) DOM 업데이트를 수행한 이후 불러낸다.
useEffect를 컴포넌트 안에서 불러내는 이유
- 컴포넌트 내부에 둠으로써 effect를 통해 count state 변수에 접근 할 수 있게 된다.
useEffect는 렌더링 이후에 매번 수행하나?
- yes, 기본적으로 첫번째 렌더링과 이후의 모든 업데이트에서 수행된다.
- 리액트는 effect가 수행되는 시점에 이미 DOM이 업데이트 되었음을 보장한다.
* componentDidMount 혹은 componentDidUpdate 와는 달리 useEffect에서 사용되는 effect는 브라우저가 화면을 업데이트 하는 것을 차단 하지 않는다. 애플리케이션의 반응성이 향상된다. 흔지 않지만 레이아웃의 측청 같은 동기적 실행이 필요한 경우 useEffect와 동일한 API를 사용하는 useLayoutEffect라는 별도의 Hook을 사용하면 된다.
Clean-up을 이용하는 effect
- 외부 데이터에 구독을 설정해야하는 경우 , 메모리 누수가 발생하지 않도록 clean-up 하는것이 중요하다
Class에서의 clean-up
- componentDidMount에 구독을 설정한 뒤 componentWillUnmount에서 이를 정리(clean-up) 한다.
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';
}
}
Hook를 이용하는 예시
- 구독과 제거를 위한 코드는 결합도가 높기 때문에 useEffect는 이를 함께 다루도록 고안되었다.
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
- 모든 effect는 정리를 위한 함수를 반환할 수 있다.
- 위 처럼 구독의 추가를 하나의 effect로 구성하면서 추가와 제거를 위한 로직을 가까이 묶어둘 수 있게 한다.
리액트가 effect를 정리 하는 시점
- 리액트는 컴포넌트가 마운트 해제되는 때에 정리(clean-up)을 실행한다. 렌더링이 실행되는 때마다 실행된다.
* effect를 반드시 named function으로 반환해야 하는 것은 아니다. 목적을 분명히 하기 위해 clean-up이라고 부르고 있다. 다른 이름으로 혹은 익명함수로 반환해도 무방하다.
관심사를 구분하려고 한다면 Multiple Effect를 사용하자
- Hook가 탄생한 동기중 하나가 생명주기 class 메소드가 관련이 없는 로직들을 모아놓고 관련 있는 로직들은 여러개의 메소드에서 나누어 놓는 경우가 자주 있다는 것이다.
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
// ...
- document.title을 설정하는 로직이 componentDidMount와 componentDidUpdate에 나누어져 있다. 구독 로직 또한 ComponentDidMount와 componentWillUnmount에 나누어 져 있다.
- componentDidMount가 두가지의 작업을 위한 코드를 모두 가지고 있는 상황이다.
Hook를 사용하여 문제를 해결하기
- State Hook를 여러 번 사용할 수 있는것 처럼 effect 또한 여러번 사용할 수 있다.
- Effect를 이용하여 서로 관련 없는 로직들을 갈라놓을 수 있다.
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
- Hook를 이용하면 생명주기 메소드에 따라서가 아닌 코드가 무엇을 하는지에 따라 나눌 수 있다.
Effect가 업데이트 시마다 실행되는 이유
- effect 정리(clean-up) 이 마운트 해제되는 때에 한번만이 아니라 모든 렌더랑 시에 실행된다.
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
// 위 class 컴포넌트 코드는 this.props로 부터
// friend.id를 읽어내고 컴포넌트가 마운트된
// 이후에 친구의 상태를 구독하며 컴포넌트가 마운트를
// 해제 할 때에 구독을 해지합니다.
- 하지만 위 코드는 컴포넌트가 화면에 표시되어 있는 동안 friend prop이 변한다 해도 이를 반영하지 못한다. 버그이다.
- 클래스 컴포넌트에서는 이런 경우를 다루기 위해 componentDidUpdate를 사용한다.
componentDidUpdate(prevProps) {
// 이전 friend.id에서 구독을 해지합니다.
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// 다음 friend.id를 구독합니다.
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
- componentDidUpdate를 제대로 다루지 않는것이 리액트 애플리케이션의 흔한 버그 중 하나다.
Hook를 사용하여 update 인지하기
omponentDidUpdate(prevProps) {
// 이전 friend.id에서 구독을 해지합니다.
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// 다음 friend.id를 구독합니다.
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
- useEffect가 기본적으로 update를 다루기 때문에 더는 업데이트를 위한 특별한 코드가 필요없다.
- 다음 effect를 적용하기 전에 이전의 effect를 clean-up 한다.
- 일관성을 유지해주며 클래스 컴포넌트에서 흔히 빼먹는 업데이트로직을 자동으로 해줌으로써 버그를 예방한다.
Effect를 skip 하고 성능 최적화하기
- 모든 렌더링 이후에 effect를 clean-up 하거나 적용하는 것이 성능 저하를 발생 시키는 경우도 있다.
- 클래스 컴포넌트 같은 경우 componentDidUpdate 에서 prevProps나 prevState와의 비교를 통해 이문제를 해결할 수 있다.
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
- useEffect Hook API에서는 이미 내재하였다. 특정 값이 리렌더링 시 변경되지 않는다면 건더뛰게 할 수 있다.
- useEffect함수의 옵션인 두 번째 인수로 배열을 넘기면 된다.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // count가 바뀔 때만 effect를 재실행합니다.
- 위 코드에서 [count]를 두번째 인수로 넘깁니다. 이것을 의미하는 것은 만약 count 가 5이고 컴포넌트가 렌더링 된 이후에도 여전히 count 가 5 라면 리액트는 이전 렌더링 시의 값을 그 다음 렌더링 떄의 5와 비교하여 effect를 건너뛰게 한다.
- 만약 6 이라면 리액트는 effect를 재실행 한다.
- 배열내에 여러값이 있다면 그 중 단 하나의 값만 다를지라도 리액트는 effect를 재실행 한다.
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // props.friend.id가 바뀔 때만 재구독합니다.
- 정리를 사용하는 effect의 경우에도 동일하게 작용한다.
* effect를 실행하고 이를 정리하는 과정을 마운트와 마운트 해제시에 딱 한번씩만 실행하고 싶다면 빈 배열 [] 을 두번째 인수로 넘긴다.
리액트로 하여금 이 effect가 prop이나 state의 그 어떤값에도 의존하지 않기때문에 재실행되어야 할 필요가 없을을 알게 한다.
'Web > React.js' 카테고리의 다른 글
[STUDY] 커스텀 Hooks (0) | 2021.03.27 |
---|---|
[STUDY] Hook의 규칙 (0) | 2021.03.23 |
[STUDY] State Hook 사용하기 (0) | 2021.03.21 |
[STUDY] Hook 개요 (0) | 2021.03.19 |
[STUDY] Hook 소개 (0) | 2021.03.18 |
댓글