나는 useEffect를 잘못 사용하고 있었다
프론트엔드 개발을 하다보면 useEffect가 사용된 코드를
꽤나 자주 목격하게 되는데요.
과거 1년차 개발자의 저는 useEffect에 다소 의존적인 개발을 해왔지만
2년차인 지금은 최대한 useEffect에 대한 의존도를 줄이려고 시도중입니다.
그렇다면 왜, useEffect를 줄여나가려고 하는 것이고
useEffect를 줄임으로서 무엇을 얻었을까요?
useEffect에 기반한 개발을 했을 때
1. 내가 원하는 동작이 무엇인지 동료가 이해하기 힘들다.
한두개의 useEffect를 사용한다면(= 내용이 간결하다는 전제)
그래도 동료가 어떤 방식으로 코드를 작성했구나 라는 흐름 파악이 가능하지만
useEffect가 3-4개가 하나의 컴포넌트에 존재한다면?
jsx에서 HTML 부분을 보고 함수를 보고 useEffect 구문을 보고…
업무 피로도가 쌓이겠다는 생각이 들었습니다.
저는 개발하고 끝! 이겠지만 저의 코드를 리뷰해야하는 동료는
HTML과 함수 useEffect로 작성된 부분을 최소 12번을 왕복하며
이해해야 되거든요.
2. 사이드 이펙트 추적이 어려워진다.
한 컴포넌트에만 useEffect가 몰려있다면 그나마 낫겠지만
다른 컴포넌트에서도 useEffect에 기반한 개발을 진행한다면?
하필 또 useEffect 내에서 상태를 업데이트를 한다면?
가뜩이나 상태 업데이트는 비동기적으로 처리되는데
업데이트 순서나 타이밍 문제로 인해 예상치 못한 동작이 발생하겠죠.
3. 불필요한 리렌더링을 발생시킬 수 있다.
의존성 배열에 포함된 변수가 매 렌더링마다 새로운 객체나 배열을 생성하는 경우엔
useEffect가 매번 실행이 됩니다. (이 문제는 1차적으론 useCallback이나 useMemo를 활용하면 되긴 하겠지만요.)
잦은 리렌더링은 새로운 데이터나 상태가 메모리에 저장되며 메모리 사용량이 증가할 수 있고,
네트워크와 연관이 있다면(ex. API 호출) 불필요하게 네트워크 요청이 증가하면서 서버에 부하를 가할 수 있겠죠.
이벤트 핸들러에서 최대한 처리해보자!
사내 코드에는 복잡한 흐름을 가진 useEffect가 다수 존재했습니다.
비슷한 하나의 예시를 보여드리며 어떤 변화가 생겼는지 볼까요?
**// 변경 전. 4개의 useEffect와 연관된 함수들을 모두 확인해야함.**
function Form({ onSubmit }: FormProps) {
...
// 첫번째 useEffect. email 값을 관리하는 context로 초기화
useEffect(() => {
reset(globalEmailData);
}, [emailCtx, reset]);
// 두번째 useEffect. 사파리 환경에서 email 값이 날아가는 현상이 발생해 setValue를 통해 강제로 값을 주입
useEffect(() => {
setValue('email', emailState || globalEmailData.email);
}, [emailCodeSent]);
// 세번째 useEffect. 이메일 input의 dropbox 기능 구현
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (inputRef.current && !inputRef.current.contains(target)) {
setActiveDropbox(false);
}};
document.addEventListener('mousedown', handleClickOutside);
}, [inputRef]);
//네번째 useEffect. 이메일이 변경될 때마다 실행되는 함수.
useEffect(() => {
onChangeEmail();
}, [email]);
return (
<>
<Box as="form" id="email" w="100%" onSubmit={handleSubmit(onSubmit)}>
</Box>
</>
)
}
export default Form;
변경 후!
**// 변경 후. Email input과 드롭박스 외부클릭에 대한 useEffect를 제거.**
function Form({ onSubmit }: FormProps) {
// 이벤트 핸들러에 함수를 추가
const onChangeEmail = (e: { target: { value: string } }) => {
const { value } = e.target;
if (value?.includes('@')) {
setActiveDropbox(true);
setEmailList(frequencyEmails.filter((el) => el.includes(value.split('@')[1])),);
} else {
setActiveDropbox(false);
setTouchKeyboard(-1);
}
};
const handleDropDownClick = (first: string, second: string) => {
setEmailState(`${first.split('@')[0]}${second}`);
setActiveDropbox(false);
setTouchKeyboard(-1);
};
// 이벤트 핸들러에 함수를 추가
const handleClickOutside = (e: React.FocusEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
if (inputRef.current && !inputRef.current.contains(target)) {
setActiveDropbox(false);
}
};
useEffect(() => {
reset(emailCtx);
}, [emailCtx, reset]);
useEffect(() => {
setValue('email', emailState || emailCtx.email);
}, [emailCodeSent]);
return (
<>
<Box as="form" id="email" w="100%" onSubmit={handleSubmit(onSubmit)}>
</Box>
</>
)
}
export default Form;
4개의 useEffect 중, 2개의 useEffect는
이벤트 핸들러만으로도 충분히 기능을 구현할 수 있었습니다.
함수명이 긴 것은 이후 커스텀 훅으로 따로 분리를 진행했어요.
이벤트 리스너로 함수를 호출함으로서 제가 의도하는 바가 무엇인지 조금은 표현을 할 수 있었고,
코드의 가독성도 나름(?!) 챙길 수 있게 되었습니다.
React 공식 문서에서도 렌더링을 위해 데이터를 변환하거나, 사용자 이벤트를 처리하는 데
useEffect를 통한 개발보단, 이벤트 핸들러를 통한 개발을 진행하라고 명시되어 있던 것을
스쳐 지나가듯이 봤었는데 이번 기회에 복습을 하게 되었습니다.
불필요한 리렌더링이나 사이드 이펙트를 방지할 수 있도록 useEffect를 배제하는 코드를
완벽히 작성하진 못했지만 지속적인 리팩토링과 다른 분들의 기술 블로그를 참고하며
동료 개발자가 이해하기 쉽고, 사이드 이펙트가 없도록
개발해야겠다는 마음이 많이 드는 시간이었습니다.
Reference
[useEffect 잘못 쓰고 계신겁니다] - velog