useEffect dependency에 useRef를 담기보단 Callback Refs를...
React base의 프로젝트에서 작업 중 DOM 노드의 크기에 따라 state 값이 바뀌는 로직을 짜야 되는 경우가 종종 발생합니다. 그럴 때 "useEffect의 dependency에 useRef의 current를 맵핑하는 것이 맞나?"라는 생각을 하게 됩니다. useRef에선 리렌더링이 일어나기 전까진 값의 변화 추적이 안되기 때문에 문제가 발생할 수 있습니다. 해당 케이스에선 어떻게 처리할지 알아보겠습니다.
useEffect dependency로 ref 연결 케이스
확인을 위한 예시코드를 작성해보겠습니다.
import { useEffect, useRef, useState } from 'react';
...
function Component() {
const [isBasicState, setIsBasicState] = useState<boolean>(false);
const [isConditionState, setIsConditionState] = useState<boolean>(false);
const { isLoading, isSuccess, data } = useGetData();
const basicEl = useRef<HTMLDivElement>(null);
const conditionalEl = useRef<HTMLDivElement>(null);
...
useEffect(() => {
if (basicEl.current) {
setIsBasicState(true);
}
}, [basicEl.current]);
useEffect(() => {
if (conditionalEl.current) {
setIsConditionState(true);
}
}, [conditionalEl.current]);
return (
<>
<div className="basic" ref={basicEl}>
{isBasicState && 'Basic state confirm'}
</div>
{isLoading && <Loading />}
{isSuccess && (
<div className="condition" ref={conditionalEl}>
{isConditionalEl && 'Condition state confirm'}
</div>
)}
</>
);
}
export default Component;
작업중에 흔히 볼 수 있는 코드로 컴포넌트가 렌더링시 존재하는 basic div element와 서버로부터 데이터를 받아와 Status가 Success일 경우 렌더링되는 condition div element를 구현했습니다. 해당 코드의 결과 다음과 같은 화면을 볼 수 있습니다.
컴포넌트 렌더링 시 basic div element가 렌더링 되며 basicEl에 해당 참조 값이 담기게 되지만 condition div element의 경우 서버의 상태 값이 아직 Success 되기 전이라 conditionalEl에는 아무 값도 담기지 않습니다. useEffect는 컴포넌트 초기 렌더링 시 한 번 실행되기 때문에 basicEl에 값이 있어 isBasicState의 값이 true가 되지만 conditionalEl에는 값이 없기 때문에 isConditionState은 그대로 false를 가집니다. 이후 서버 상태 값이 Success가 되어 conditionalEl에 참조 값이 담기더라도 해당 값을 dependency로 가지는 useEffect는 해당 useRef의 변화 추적이 안되기 때문에 실행되지 않습니다.
그 후 렌더링이 발생할 경우 conditionalEl에도 참조 값이 담기게 되지만 컴포넌트가 초기 렌더링 이후 리렌더링이 발생하지 않을 경우 원하는 동작을 할 수 없습니다.
Callback Refs
위와 같은 케이스를 막기 위해 useCallback을 이용한 Callback Refs를 만들어 사용합니다. 확인을 위한 예시 코드는 다음과 같습니다.
import { useCallback, useState } from 'react';
...
function Component() {
const [isBasicState, setIsBasicState] = useState<boolean>(false);
const [isConditionState, setIsConditionState] = useState<boolean>(false);
const { isLoading, isSuccess, data } = useGetData();
const basicEl = useCallback((el: HTMLDivElement) => {
if (!el) return;
setIsBasicState(true);
}, []);
const conditionalEl = useCallback((el: HTMLDivElement) => {
if (!el) return;
setIsConditionState(true);
}, []);
...
return (
<>
<div className="basic" ref={basicEl}>
{isBasicState && 'Basic state confirm'}
</div>
{isLoading && <Loading />}
{isSuccess && (
<div className="condition" ref={conditionalEl}>
{isConditionalEl && 'Condition state confirm'}
</div>
)}
</>
);
}
export default Component;
useRef 대신 useCallback을 이용하여 element가 렌더링 될 때 해당 callback을 실행시키도록 구현했습니다. 해당 코드의 결과 다음과 같은 화면을 볼 수 있습니다.
이전과 달리 element가 렌더링 될 때 callback이 발생했기 때문에 서버의 상태 값이 Success가 되어 해당 element가 렌더링 될 때 원하는 동작을 할 수 있었습니다.
그리고 useCallback을 이용하여 element가 렌더링 될 때마다 해당 callback이 실행되지 않도록 dependency를 걸어둘 수 있어 최적화에도 용이합니다.
마무리
useRef의 값을 useEffect의 dependency로 걸면 해당 useEffect가 컴포넌트의 모든 리렌더링 조건을 가지고 있다면 이슈가 없지만 일일이 체크하는 것보단 Callback Refs를 만들어 사용하는 것이 좋겠습니다.
개발을 하며 의문점이 드는 부분은 다른 개발자들도 비슷하게 의문을 가지며 해답을 찾아가는 것 같습니다. 늘 그렇듯 공식 문서를 좀 더 잘 찾아봐야겠습니다.
참고 자료
https://react-ko.dev/reference/react-dom/components/common#ref-callback