아래 에러 메시지는 React 개발자들에게는 꽤 낯익을 것 같다. 언마운트된 컴포넌트에 대해 수행할 수 없는 상태 업데이트가 일어나고 있으며 이는 메모리 누수를 나타낸다는 뜻이다. 일반적으로 이 문제를 해결하기 위해서는 useEffect의 cleanup 함수를 사용해 모든 구독과 비동기 작업을 취소하면 된다.
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 function.
입력을 마치고 폼을 제출한 후 다음 페이지로 이동하는 시점에서 위 에러가 발생했으므로 컴포넌트에 선언한 상태 중 navigate로 이동한 후에 업데이트되는 상태가 있는지 확인하기로 했다.
문제는 아무리 살펴봐도 해당 컴포넌트에 선언한 상태 중 언마운트된 후에 업데이트되는 상태가 없었다는 점이다. 이 시점에서 잘못된 선택을 하게 되는데 에러 메시지로 되돌아가는 것이 아니라 무턱대고 구글링을 시작하고 말았다.
안타깝게도 한참을 엉뚱한 곳에서 헤맨 후에야 아래 원칙을 떠올리고 원점으로 돌아갔다.
길을 잃었을 때는 제자리로 돌아가야 한다.
결국 스택 트레이스를 자세히 살펴봄으로써 해결할 수 있었다. 노란 박스 안에 있는 링크를 클릭해 첫 번째 에러 발생 지점의 코드를 살펴보기로 했다.
에러가 발생한 지점은 antd-mobile의 Button 컴포넌트 내 innerLoading 상태(노란 박스 참고)였다. 이 부분을 보자마자 Button 컴포넌트 문서에서 읽었던 loading과 onClick에 전달되는 비동기 함수 사이의 관계가 기억났다.
파란 박스친 부분의 코드를 더 살펴보면 onClick에 promise가 전달될 경우 innerLoading 상태를 true로 바꾸고 promise 실행이 완료되면 다시 false로 바꾸는 것을 볼 수 있다.
결과적으로 onClick에 비동기 함수를 전달하는 것이 아니라 내부에서 비동기 함수를 선언하고 호출함으로써 에러 메시지를 없앨 수 있었다.
import { Button } from "antd-mobile";
import { useForm } from "react-hook-form";
export default function ExampleComponent() {
const { ..., handleSubmit } = useForm();
const onSubmit = () => {
const submit = async () => { // 내부에서 비동기 함수 선언
try {
const response = await ...
}
};
submit(); // 비동기 함수 호출
// promise를 반환하지 않으므로 innerLoading 상태가 업데이트되지 않게 됨
}
return (
...
<Button onClick={handleSubmit(onSubmit)}>
확인
</Button>
)
}