useEffect일 것입니다. 비동기 작업을 수행할 수 있게 해주지만 이름부터 난해한 이 Hook은 훌륭한 기능을 제공하지만, 동시에 많은 문제를 일으키기도 합니다. 특히 서버에서 데이터를 계속해서 가져오는 무한 루프 현상이 대표적입니다.
useEffectEvent라는 새로운 Hook을 고안해냈습니다. 이름이 다소 길고 복잡하게 느껴질 수 있지만, 여러분의 React 앱을 안정화하는 데 있어 구세주와 같은 존재입니다.useEffectEvent가 이를 어떻게 해결하는지 보여드리겠습니다.useEffect 사용 사례useEffectEvent 같은 새로운 Hook이 중요한 이유입니다. 컴파일러의 경우 객체 참조를 안정화하여 객체 정체성과 관련된 잠재적 버그를 줄여줍니다. 그리고 useEffectEvent는 의존성 배열에서 객체를 완전히 제거해 버립니다!function MyUserInfo() { const [userName, setUserName] = useState("Bob"); return ( <div> <input value={userName} onChange={(evt) => setUserName(evt.target.value)} /> </div> );}function MyUserInfo() { const [userName, setUserName] = useState("Bob"); useEffect(() => { let loggedInTime = 0; const interval = setInterval(() => { loggedInTime++; }, 1000); return () => clearInterval(interval); }, []); return ( <div> <input value={userName} onChange={(evt) => setUserName(evt.target.value)} /> </div> );}[useEffect](https://blog.logrocket.com/useeffect-react-hook-complete-guide/)를 추가했습니다. (네, 실제로 로그인된 시간을 엄밀히 측정하는 것은 아니며 데모용 코드일 뿐입니다.)loginMessage 문자열을 추가해 보겠습니다.function MyUserInfo() { const [userName, setUserName] = useState("Bob"); const [loginMessage, setLoginMessage] = useState(""); useEffect(() => { let loggedInTime = 0; const interval = setInterval(() => { loggedInTime++; setLoginMessage( `${userName} has been logged in for ${loggedInTime} seconds` ); }, 1000); return () => clearInterval(interval); }, []); return ( <div> <div>{loginMessage}</div> <input value={userName} onChange={(evt) => setUserName(evt.target.value)} /> </div> );}useEffect에 전달한 함수가 "오래된(stale)" 상태가 될 수 있기 때문입니다.
input은 변경되지만, 로그인 메시지는 계속해서 사용자 이름이 "Bob"이라고 말합니다. 하지만 실제로는 이름이 바뀌었죠.useEffect에 보낸 함수는 당시의 userName 값("Bob")을 캡처한 "클로저(closure)"를 생성했습니다. 그리고 이 값은 절대 변하지 않을 것입니다. 실제 값과 동기화되지 않기 때문에 우리는 이를 "오래된(stale)" 상태라고 부릅니다. 즉, "오래된 클로저"가 발생한 것입니다.useEffectEvent는 아니니 조금만 기다려 주세요). 의존성 배열에 userName을 추가하기만 하면 됩니다.useEffect(() => { let loggedInTime = 0; const interval = setInterval(() => { loggedInTime++; setLoginMessage( `${userName} has been logged in for ${loggedInTime} seconds` ); }, 1000); return () => clearInterval(interval);}, [userName]);userName을 수정하면 로그인 메시지도 바뀝니다! 멋지네요. 아, 잠깐만요. 이게 뭐죠? 변경할 때마다 로그인 시간이 다시 1초로 돌아가 버립니다.
userName 값으로 새로운 클로저를 생성할 때마다 이전 타이머를 죽이고 있습니다(이건 좋습니다). 하지만 새로운 loggedInTime을 생성하고 다시 0부터 시작하고 있습니다. 이건 확실히 좋지 않네요.loggedInTime을 상태(state)로 관리하고 JSX에서 문자열 포맷을 맞추는 것입니다. 알겠습니다. 하지만 그렇게 할 수 없는 상황이라고 가정해 봅시다.useRef가 구원해 줄 것입니다useEffectEvent가 나오기 전이라면 아마도 ref를 사용했을 것입니다.const nameRef = useRef(userName);nameRef.current = userName;useEffect(() => { let loggedInTime = 0; const interval = setInterval(() => { loggedInTime++; setLoginMessage( `${nameRef.current} has been logged in for ${loggedInTime} seconds` ); }, 1000); return () => clearInterval(interval);}, []);userName의 현재 값을 저장하는 참조(reference)를 만들고, 매 렌더링마다 현재 값을 업데이트합니다. React는 상태처럼 ref를 모니터링하지 않기 때문에 렌더링 중에 ref의 current 값을 설정해도 괜찮습니다.userName 대신 nameRef.current를 사용합니다. 매 렌더링마다 업데이트되므로 항상 userName의 최신 값을 가져올 수 있습니다. 마지막으로 의존성 배열에서 userName을 제거하여 리셋 버그를 해결했습니다.
useEffectEvent가 등장합니다.const getName = useEffectEvent(() => userName);useEffect(() => { let loggedInTime = 0; const interval = setInterval(() => { loggedInTime++; setLoginMessage( `${getName()} has been logged in for ${loggedInTime} seconds` ); }, 1000); return () => clearInterval(interval);}, []);userName의 현재 값을 반환하는 게터(getter) 함수를 만들었습니다. 이 함수는 useEffect 내에서 호출될 수 있으며 절대 오래된 상태가 되지 않습니다. 정말 깔끔합니다. useRef 버전보다 훨씬 더 깔끔하고 명확합니다.useEffect를 더 일반적인 관점에서 생각할 수 있게 해준다는 것입니다. 생각해보면, 우리는 해당 useEffect를 통해 일종의 범용적인 "타이머"를 갖게 된 셈입니다.const onTick = useEffectEvent((tick: number) => setLoginMessage(`${userName} has been logged in for ${tick} seconds`));useEffect(() => { let ticks = 0; const interval = setInterval(() => onTick(++ticks), 1000); return () => clearInterval(interval);}, []);useEffectEvent로 옮겼습니다. useEffect가 얼마나 더 깔끔해졌는지 보이시나요? useEffect는 단지 타이머만 관리합니다. 그리고 onTick은 그 타이머로 무엇을 할지에 대한 모든 로직을 처리합니다.useEffectEvent는 게임 체인저입니다useEffect가 상태에 대한 의존성을 전혀 갖지 않는다는 것입니다. 우리가 보았듯이 useEffect가 문제를 일으키는 지점은 바로 상태 의존성입니다. 잘못된 상태에 의존하는 잘못된 의존성 배열은 오래된 클로저 문제, 부적절한 리셋, 심지어 무한 루프까지 유발할 수 있습니다. useEffectEvent를 사용하면 의존성 배열에서 상태를 제거할 수 있습니다. 이는 우리가 더 나은 useEffect를 작성하도록 도와줍니다.function useInterval(onTick: (tick: number) => void) { const onTickEvent = useEffectEvent(onTick); useEffect(() => { let ticks = 0; const interval = setInterval(() => onTickEvent(++ticks), 1000); return () => clearInterval(interval); }, []);}useInterval 구현체를 갖게 되었습니다.function useInterval(onTick: (tick: number) => void, timeout: number = 1000) { // ????}function useInterval(onTick: (tick: number) => void, timeout: number = 1000) { const onTickEvent = useEffectEvent(onTick); useEffect(() => { let ticks = 0; const interval = setInterval(() => onTickEvent(++ticks), timeout); return () => clearInterval(interval); }, [timeout]);}timeout이 바뀔 때마다 카운터가 다시 0부터 시작되므로 또다시 오래된 클로저 문제가 발생합니다. 이런. 아, 맞다, 또 다른 useEffectEvent를 사용하면 되겠네요.function useInterval(onTick: (tick: number) => void, timeout: number = 1000) { const onTickEvent = useEffectEvent(onTick); const getTimeout = useEffectEvent(() => timeout); useEffect(() => { let ticks = 0; let mounted = true; function onTick() { if (mounted) { onTickEvent(++ticks); setTimeout(onTick, getTimeout()); } } setTimeout(onTick, getTimeout()); return () => { mounted = false; }; }, []);}setInterval 대신 setTimeout을 사용하고 매 반복마다 타임아웃을 조정하고 있습니다. 이 코드를 더 줄일 수 있는 방법이 있다면 알려주세요.useEffect 코드가 통제 불능으로 실행되는 문제가 있음을 인정했습니다. 그들은 상태와 연결된 useEffect라는 문제의 원인을 명확히 파악했습니다. 그리고 useEffect를 상태로부터 분리하는 우아한 해결책을 제시했습니다.useEffectEvent 같은 개선 사항 덕분에 우리는 훨씬 더 신뢰할 수 있고 견고한 React 코드를 작성할 수 있게 되었습니다. React는 결코 죽지 않았으며, 매 릴리스마다 더 좋아지고 있습니다!useEffectEvent를 사용하여 useEffect Hook이 얼마나 더 깔끔하고 안전해질 수 있는지 확인해 보시기 바랍니다.아직 댓글이 없습니다.
첫 번째 댓글을 작성해보세요!

Code Review in the age of AI
Inkyu Oh • AI & ML-ops

라이트 모드 인플레이션
Inkyu Oh • UI/UX

TypeScript 성능 문제 해결: 사례 연구
Inkyu Oh • Front-End

시그널(Signals) vs 쿼리 기반 컴파일러(Query-Based Compilers)
Inkyu Oh • SW Engineering