🔍 배경 및 궁금증
로그인 페이지(/login)에서 로그인 버튼을 누르면 history.push로 자동으로 홈 페이지(/)로 이동할 때 아래와 같은 에러가 발생한다

react_devtools_backend.js:2528 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 function. at LoginForm (http://localhost:8000/main.bundle.js:4817:23) at LoginPage (http://localhost:8000/main.bundle.js:5169:73)
📢 해결 과정
해결에 영감을 준 자료

문제 원인
// useForm const handleSubmit = async (e) => { setIsLoading(true); e.preventDefault(); const newErrors = validate(values); if (Object.keys(newErrors).length === 0) { await onSubmit(values); } setErrors(newErrors); setIsLoading(false); };
useForm
에서setErrors
와setIsLoading
하기 전에onSubmit
함수를 호출한다
- 문제는
onSubmit
함수는handleLogin
을 전달받는데handleLogin
내부에서는history.push
로 라우팅을 하여setErrors
와setIsLoading
을 실행하기 전에 라우팅으로 인해 폼 컴포넌트가 unmount 된다는 점이다
// LoginPage const handleLogin = async (loginFields) => { try { const userInfo = await postLogin(loginFields); if (userInfo.token) { fillUserInfo(userInfo); history.push('/'); } } catch (error) { // TODO: alert를 토스트로 교체한다 // eslint-disable-next-line no-alert alert(error.response.data); } };
해결책
- 따라서
setErrors
와setIsLoading
을 모두 마무리한 후에history.push
로 라우팅 처리를 해주면 모든 게 해결된다
- 그러나 라우팅 처리를
useForm
내부로 가져오는 것은 관심사 분리 측면에서 불합리하다고 판단했다. 왜냐하면 라우팅 처리는 로그인 API(postLogin
)가 성공했을 때 실행해야 하는데useForm
은 비동기 로그인 API의 존재를 알지 못하기 때문이다.
- 그래서 우선 간단하게 처리 할 수 있는
setError
를onSubmit
실행 이전으로 순서를 변경하는 것부터 진행했다
- 로딩 처리(
setIsLoading
)는 비동기 로그인 함수가 끝난 이후에 동작해야 하는 것이 분명했으므로onSubmit
함수 안에서 라우팅 처리 뺐다
- 그리고 라우팅 처리를 로그인 API의 성공여부를 알 수 있는 컴포넌트 내에서
useEffect
를 사용하여 실행하도록 했다
// useForm const handleSubmit = async (e) => { setIsLoading(true); e.preventDefault(); const newErrors = validate(values); setErrors(newErrors); if (Object.keys(newErrors).length === 0) { await onSubmit(values); } setIsLoading(false); };
// LoginPage const { userInfo, fillUserInfo } = useUserInfo(); //... const handleLogin = async (loginFields) => { try { const newUserInfo = await postLogin(loginFields); if (newUserInfo.token) { fillUserInfo(newUserInfo); } } catch (error) { // TODO: alert를 토스트로 교체한다 // eslint-disable-next-line no-alert alert(error.response.data); } }; //... useEffect(() => { if (userInfo.token) { history.push('/'); } }, [history, userInfo.token]);
해결은 되었지만 아직 잘 이해가 안되는 부분
useForm의 setLoading과 useEffect의 실행 순서가 예상과 많이 다르다



또 다른 해결책 (왜 해결되는지는 아직 모름)
import { useState, useEffect } from 'react'; const useForm = ({ initialValues, onSubmit, validate }) => { const [values, setValues] = useState(initialValues); const [errors, setErrors] = useState({}); const [isLoading, setIsLoading] = useState(false); useEffect(() => { return () => {}; }, [isLoading]); const handleChange = (e) => { const { name, value } = e.target; setValues({ ...values, [name]: value }); }; const handleSubmit = async (e) => { setIsLoading(true); e.preventDefault(); const newErrors = validate(values); setErrors(newErrors); if (Object.keys(newErrors).length === 0) { await onSubmit(values); } setIsLoading(false); }; return { values, errors, isLoading, handleChange, handleSubmit, }; }; export default useForm;