React 17을 사용하던 중, 다음과 같은 에러를 마주했다.
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup
이 에러는.. unmount 된 컴포넌트의 setState를 조작할 때 나타났다.
찾아보니 아래와 같은 구체적인 이유가 있었다.
const TodoItem = ({ id, title, setTodos }) => {
const [isLoading, setIsLoading] = useState(false);
const handleRemoveTodo = useCallback(async () => {
try {
setIsLoading(true);
await deleteTodo(id);
setTodos((prev) => prev.filter((item) => item.id !== id));
} catch (error) {
console.error(error);
alert("Something went wrong.");
} finally {
setIsLoading(false);
}
}, [id, setTodos]);
// 나머지 코드...
};
위 코드에서, setTodos((prev) => prev.filter((item) => item.id !== id));
를 하면 상태 변경으로 인해 리스트에서 특정 아이템이 제거된다. 이 때, 해당 아이템을 렌더링하는 TodoItem
컴포넌트는 DOM에서 사라지게 되고, 그 결과 이 컴포넌트는 언마운트된다. 이후 finally
블록에서 setIsLoading(false)
을 호출하려고 할 때, 해당 컴포넌트는 이미 언마운트되었기 때문에 경고가 발생하는 것이다.
그래서 아래와 같이 마운트 상태에서만 업데이트를 실행하게 로직을 바꿔주면 에러가 사라진다.
const TodoItem = ({ id, title, setTodos }) => {
const [isLoading, setIsLoading] = useState(false);
const isMounted = useRef(true); // 컴포넌트의 마운트 상태를 추적하는 ref
useEffect(() => {
return () => {
isMounted.current = false; // 컴포넌트가 언마운트될 때 ref 값을 변경
};
}, []);
const handleRemoveTodo = useCallback(async () => {
try {
setIsLoading(true);
await deleteTodo(id);
setTodos((prev) => prev.filter((item) => item.id !== id));
} catch (error) {
console.error(error);
alert("Something went wrong.");
} finally {
if (isMounted.current) {
setIsLoading(false); // 마운트 상태에서만 상태 업데이트 실행
}
}
}, [id, setTodos]);
// 나머지 코드...
};
하지만 불필요하게 코드가 길어진다.
react-query는 아래와 같은 기능들을 수행하기 때문에 나는 react query 도입만으로 해결했다.
Query Cancellation: react-query는 쿼리가 더 이상 필요하지 않을 때 쿼리를 자동으로 취소합니다. 예를 들어, 컴포넌트가 언마운트되거나 다른 쿼리가 같은 데이터를 패치하는 경우입니다.
Error & Success Handling: react-query는 오류 및 성공 처리에 관한 내장 로직을 제공하여, 성공/실패 콜백을 쉽게 추가할 수 있습니다.
지금까지 이렇다라고 알고 있었다... 근데..........
좋은 기회로 알게된 사실이 있다. 바로 이 에러가 신경쓰지 않아도 되는 에러라는 점! (하지만 사실 에러를 신경쓰지 않을 수 없고.. 콘솔에 뜨는 것을 참을 수 없음)
React 18 Github Issue에 이런 내용이 있다.
No warning about setState on unmounted components: Previously, React warned about memory leaks when you call setState on an unmounted component. This warning was added for subscriptions, but people primarily run into it in scenarios where setting state is fine, and workarounds make the code worse. We've removed this warning.
정독을 해보니, 요약하면 아래 한 줄이다.
There's no "memory leak" here
...
조금 더 추가하자면.. 내가 위에 mount 를 검사하는 조건을 추가한 코드는 결국
Just adding if (!isMounted) does not unsubscribe you from anything. In fact there is no way to "unsubscribe" from a Promise. So what you're describing is already how the code that tries to avoid the warning works today. For these cases, the warning is not effective in any way. And like I said earlier, there's no particular concern because it only lives as long as the fetch itself.
효과가 없고 할필요도 없다고 한다. 이런 경우엔 이들이 지향하는 커스텀 훅을 쓰도록 하자.
하지만 그래도 광범위하게 보았을때 메모리 누수가 일어날 수도 있는 환경일 수 있으므로 해당 warning이 뜬다고 한다. 이 경고가 유용한 경우는 addEventListner + removeEventListener
를 쓰는 경우라고 한다.
검색하면 죄다 단편적인 해결방법만 알려주고.. 이런 이슈를 단번에 찾기 어려우므로 포스팅을 해보았다. 누군가에게 도움이 되길~~~