useEffect의 clean-up 함수
일전에 블로깅을 위해 useEffect 훅에 대해 추가 복습을 하다 보니 문득 clean-up 함수 라는 단어가 눈에 들어왔습니다. 사실 전에 useEffect의 사용성에 대해 배울 때는 이 부분에 대해 아예 건너뛰었거든요. 처음 보는 기능이라는 생각이 들어 들여다보다가 이번 스터디 주제로 삼아보면 어떨까 했습니다.
물론, 그 전에 가볍게 useEffect 훅에 대해 훑고 가는 게 좋을 것 같아요.
useEffect(콜백함수, [Dependency])
위 코드는 우리가 배운 useEffect의 기본 구조입니다. 두 가지 인자를 전달받고 있죠. 하나는 처음 한번 실행되고 이후 렌더링이 발생할 때마다 실행할 콜백함수, 그리고 다른 하나는 의존성 배열입니다.
그런데 useEffect는 아래와 같이 더 심화된 모양으로 작성할 수 있습니다. 위와 비슷한 듯 하지만 가운데 return으로 함수를 하나 반환하고 있는 것을 볼 수 있습니다.
useEffect(() => {
console.log('컴포넌트가 화면에 나타남');
return () => {
console.log('컴포넌트가 화면에서 사라짐');
};
}, []);
🚩clean-up 함수란 무엇이고, 어디에 쓰는 것이냐?
clean-up 함수는 useEffect() 함수 내부에서 return되는 함수를 말합니다. useEffect()를 끝내며 실행되는 함수라서 clean-up이라는 이름을 붙였습니다.
useEffect(() => {
console.log('componentDidMount')
return function componentWillUnmount() {
//함수를 리턴해준다, 여기가 clean-up 함수, 이름은 다르게 짓거나 화살표 함수도 가능
console.log('componentWillUnmount')
}
}, [])
사실 useEffect의 ‘effect’는 side effect라는 함수형 프로그래밍 용어를 나타내는데요. 일반적인 side effect로는 백엔드 서버에 API로 데이터 요청하기, 브라우저 API상호작용, setTimeout이나 setInterval등 예측하기 힘든 타이밍 함수를 사용하는 경우가 있습니다. 이런 side effect들을 처리하기 위해 등장한 것이 useEffect 훅이고요.
그리고 보통 useEffect에서 처리하는 side effect에는 두 가지가 존재합니다.
'정리가 필요하지 않거나', 그리고 '정리가 필요하거나'.
정리가 필요없는 이펙트는 API 요청, 리액트 컴포넌트의 돔 수동조작, 로깅 등으로 일단 실행이 되면 신경 쓸 게 따로 없는 이펙트들입니다. 이런 경우 useEffect 내부에서 함수를 반환할 것 없이 간단하게 쓰면 됩니다.
그러나 때로는 정리가 필요한 effect도 존재합니다. 예를 들자면 WebSocket을 사용해 외부 데이터 구독하기(구독은 사용하지 않을 때 꺼줘야 합니다), 혹은 setInterval 함수를 사용하는 타이머(clearInterval함수를 써서 clean up 하지 않으면 컴포넌트가 언마운트 된 후에도 계속 실행됩니다.)등이 있는데, 이럴 때 더 실행할 필요가 없는 side effect를 중지 시켜주지 않으면 계속 메모리를 차지합니다. 따라서 메모리에 누수가 발생하지 않게 정리를 해줘야 하는 것이죠.
이럴 때 clean-up 함수에 이벤트를 해제하기 위한 작업을 넣어줍니다.
기존까지 클래스형 컴포넌트에서는 생명 주기 메소드를 사용해 문제를 해결했습니다. componentDidMount 메서드에 구독을 설정해둔 뒤, componentWillUnmount 에서 이것을 정리해 주었죠.
보다시피 동일 이펙트에 관련 되어 있으나 별개로 분리해 적은 것을 볼 수 있습니다.
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
});
}
이것을 함수형 컴포넌트의 useEffect 에서는 결합하여 사용할 수 있도록 만들었습니다. useEffect가 함수를 반환하면 리액트는 이 함수를 정리가 필요할 때 실행시킵니다. 즉, 이펙트의 추가와 제거가 하나의 useEffect 내부에 존재합니다.
아래 코드처럼 반환되는 clean-up 함수는, 클래스형 컴포넌트의 componentWillUnmount와 비슷한 기능을 담당합니다.
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);
};
});
아래와 같은 setInterval 코드는 어떨까요. 이 경우, 컴포넌트가 사라진 뒤에도 setInterval이 더는 존재하지 않는 상태를 업데이트 하려 시도합니다. 이것이 바로 메모리 누수라고 불리는 오류인데 이럴 때 함수를 반환하여 중지시켜줍니다.
function Timer() {
const [time, setTime] = useState(0);
useEffect(() => {
setInterval(() => setTime(1), 1000);
//1초에 한번씩 실행 타이머
}, []);
}
컴포넌트가 언마운트 될 때에는 반복되는 setInterval을 멈춰야 합니다! 아래와 같이 return으로 clearInterval을 보내줍니다.
function Timer() {
const [time, setTime] = useState(0);
useEffect(() => {
let interval = setInterval(() => setTime(1), 1000);
return () => {
//clean-up함수
clearInterval(interval);
}
}, []);
}
🚩그렇다면 정리(clean-up)하는 시점은 언제일까요?
React는 컴포넌트가 언마운트 되는 때에 정리(clean-up)를 실행합니다.
그런데 위의 예시에서 보았듯이 effect는 한번이 아니라 매번 렌더링 될 때마다 동작 합니다. 따라서 리액트는, 다음 렌더링이 일어나기 전에 이전 렌더링에서 파생된 effect를 clean up 한다고 합니다. 원래 effect는 의존성 배열(dependency)값이 바뀔 때마다 작동되도록 짜여 있죠.
Effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time. (출처: 공식 문서)
따라서 useEffect의 dependency 값이 변화하는 시점마다 clean-up를 호출한다고 보면 됩니다.
결론적으로 useEffect의 clean-up 함수는 이펙트를 되돌리기 위해 사용됩니다. useEffect 내 함수가 여러 번 실행될 경우 다음번 useEffect가 실행되기 전에 clean up을 진행하며, 더 효율적으로 useEffect가 작동할 수 있게 하여 메모리를 잘 관리해줍니다.
참고로 하나 더 적자면 react16까지 clean up 함수는 동기적으로 동작했다고 합니다. 하지만 react17부터 clean up 함수는 항상 비동기적인 실행을 합니다. 즉 컴포넌트가 언마운트되면, 화면이 업데이트한 직후 clean up 함수가 실행됩니다.
+ 포스팅 적다 발견한 추가 적인 팁:
간혹 useEffect를 쓸 때 clean-up 함수가 작동하지 않는 경우가 발생하기도 합니다. 이럴 땐 혹시 async를 붙여서 비동기 처리를 하고 있지 않은지 살펴봐야 합니다. async 함수를 쓰면 함수를 반환하게 코드를 써도 Promise 객체를 리턴하고 맙니다. 따라서 useEffect에 async 키워드를 붙이면 clean up 함수가 제대로 작동하지 않습니다. async를 지우면 해결되는 문제지만, 만약 꼭 비동기 처리와 clean-up을 둘다 쓰고 싶다면 useEffect를 여러 번 작성해서 두 기능을 분리해줄 수 있습니다.
참고:
https://choar816.tistory.com/166
https://ko.legacy.reactjs.org/docs/hooks-effect.html
https://overreacted.io/ko/a-complete-guide-to-useeffect/
https://dev.to/pallymore/clean-up-async-requests-in-useeffect-hooks-90h
리액트에서의 side effect: 리액트 컴포넌트가 화면에 렌더링이 된 후, 비동기로 처리되어야 하는 부수 효과들.
메모리 누수: 애플리케이션으로 인해 발생하는 Windows 메모리 손실