useRef, useEffect에 대해 좀 더 이해해보기 위해 예제를 만들어 공부 중이다.
궁금한 점은 두 가지 정도이다.
1. ref의 변경이 useEffect의 재실행을 트리거하는가
2. props의 변경이 반드시 해당 컴포넌트의 리렌더링을 유발하는가?
먼저 첫 번째 궁금증을 해결해보자.
export default function RefTest() {
const countRef = useRef<{ count: number }>({ count: 0 });
console.log("컴포넌트 실행", countRef.current.count);
useEffect(() => {
console.log("useEffect실행", countRef.current.count);
}, [countRef.current]);
return (
<div>
<span>{countRef.current.count}</span>
<button
onClick={() => {
countRef.current = {
...countRef.current,
count: countRef.current.count + 1,
};
console.log(countRef.current);
}}
>
Ref Update
</button>
</div>
);
}
만약 ref의 변경이 useEffect의 콜백 실행을 트리거한다면, 안에 있는 콘솔이 실행되어야 할 것이다.
하지만 다음과 같이 컴포넌트 내부에 있는 첫번째 콘솔과 useEffect 내부에 있는 두번째 콘솔이 실행되지는 않고 클릭 이벤트에 있는 세번째 콘솔만 실행되는 것을 볼 수 있다.
즉, ref의 변경은 useEffect를 트리거할 수 없다는 결론을 얻을 수 있다.
이번에는 props의 변경이 무조건 컴포넌트의 리렌더링을 유발하는가에 대해서 확인해보도록 하자.
export default function RefTest() {
const countRef = useRef<{ count: number }>({ count: 0 });
console.log("컴포넌트 실행", countRef.current.count);
useEffect(() => {
console.log("useEffect실행", countRef.current.count);
}, [countRef.current]);
return (
<div>
<span>{countRef.current.count}</span>
<button
onClick={() => {
countRef.current = {
...countRef.current,
count: countRef.current.count + 1,
};
console.log(countRef.current);
}}
>
Ref Update
</button>
<RefTestChild count={countRef.current.count} />
</div>
);
}
function RefTestChild({ count }: { count: number }) {
console.log("child 컴포넌트 실행", count);
useEffect(() => {
console.log("child useEffect실행", count);
}, [count]);
return <div>{count}</div>;
}
만약 props의 변경이 무조건 해당 props를 받는 컴포넌트의 리렌더링을 유발한다면, RefTestChild의 콘솔이 실행되어야 할 것이다.
하지만 실제로 실행해보면 props를 주입받는 컴포넌트의 리렌더링도 일어나지 않는다.
그렇다면 기존에 대규모 프로젝트에서 ref를 변경했는데도 컴포넌트가 리렌더링되거나 useEffect가 실행되는 이유는 무엇이었을까? 그건 기존 컴포넌트의 이벤트에 state를 업데이트하는 코드도 같이 있었을 가능성이 높았기 때문이라고 생각한다. 다음과 같은 경우이다.
export default function RefTest() {
const [updatedCount, setUpdatedCount] = useState(0);
const countRef = useRef<{ count: number }>({ count: 0 });
console.log("컴포넌트 실행", countRef.current.count);
// ref 값으로는 useEffect 실행 X / 외부 state가 변경되었기 때문에 ref의 current가 동작하는 것처럼 보이는 현상이었던 것
useEffect(() => {
console.log("useEffect실행", countRef.current.count);
}, [countRef.current]);
return (
<div>
<span>{countRef.current.count}</span>
<button
onClick={() => {
countRef.current = {
...countRef.current,
count: countRef.current.count + 1,
};
console.log(countRef.current);
}}
>
Ref Update
</button>
<button
onClick={() => {
setUpdatedCount((prev) => prev + 1);
}}
>
State Change
</button>
</div>
);
}
Ref의 변경 ➡️ useEffect 실행 X
Ref의 변경 ➡️ State의 변경 ➡️ useEffect 실행
State의 변경 ➡️ Ref의 변경 ➡️ useEffect 실행 X
State의 변경 ➡️ Ref의 변경 ➡️ State의 변경 ➡️ useEffect 실행
즉, State의 변경이 마지막에 이뤄지게 되면 컴포넌트가 리렌더링되고 그 과정에서 ref의 최신화가 이뤄지게 된다고 정리할 수 있을 것 같다.