문제
이런 코드가 있다고 하자.
const storedSizeRef = useRef(getStoredSize());storedSizeRef.current는 첫 렌더의 값으로 유지된다.
그런데 getStoredSize() 자체는 컴포넌트가 렌더될 때마다 다시 호출된다.
처음에는 조금 이상해 보인다.
컴포넌트는 매 렌더마다 다시 실행된다
React 함수 컴포넌트는 말 그대로 함수다.
리렌더링이 발생하면 React는 컴포넌트 함수를 다시 호출한다.
function Component() {
const storedSizeRef = useRef(getStoredSize());
return null;
}그래서 렌더가 발생할 때마다 함수 본문의 코드도 다시 실행된다.
함수의 인자는 먼저 계산된다
JavaScript는 함수를 호출하기 전에 인자를 먼저 평가한다.
useRef(getStoredSize());이 코드는 실행 순서상 다음과 비슷하다.
const value = getStoredSize();
useRef(value);즉 React의 useRef가 실행되기 전에 이미 getStoredSize()가 호출된 상태다.
React가 초기값을 무시할 수는 있어도, 이미 발생한 함수 호출까지 막을 수는 없다.
useRef가 유지하는 것은 값이 아니라 ref 객체다
첫 렌더에서 useRef는 대략 이런 객체를 만든다.
{
current: initialValue
}이후 렌더에서는 새 객체를 만들지 않고 이전 ref 객체를 그대로 반환한다.
// 첫 렌더
const value = getStoredSize();
const ref = { current: value };
// 이후 렌더
const unusedValue = getStoredSize();
return previousRef;따라서 결과는 다음과 같다.
getStoredSize() 호출: 매 렌더 발생
storedSizeRef.current: 첫 렌더의 값 유지useState의 lazy initializer
const [storedSize] = useState(() => getStoredSize());여기서는 getStoredSize()의 결과가 아니라 함수 자체를 React에 전달한다.
const initializer = () => getStoredSize();
useState(initializer);React가 이 initializer를 마운트할 때만 실행하기 때문에, getStoredSize()도 초기 렌더에서만 호출된다.
반면 아래 코드는 lazy initializer가 아니다.
useState(getStoredSize());이 경우에도 함수 인자가 먼저 계산되므로 getStoredSize()는 매 렌더마다 호출된다.
useRef를 한 번만 초기화하려면
const storedSizeRef = useRef<Size | null>(null);
if (storedSizeRef.current === null) {
storedSizeRef.current = getStoredSize();
}이렇게 하면 실제 값이 없을 때만 getStoredSize()가 실행된다.
값이 UI 렌더링에 사용되고, 변경 시 화면도 다시 그려야 한다면 useState가 더 자연스럽다.
const [storedSize] = useState(() => getStoredSize());정리
useRef(getStoredSize())에서 함수가 반복 호출되는 이유는 useRef의 특수한 동작 때문이라기보다 JavaScript의 함수 인자 평가 순서 때문이다.
const storedSizeRef = useRef(getStoredSize());React가 받는 시점에는 이미 getStoredSize()가 실행된 뒤다.
useRef는 이후 렌더에서 새 초기값을 사용하지 않을 뿐, 초기값을 계산하는 표현식까지 실행하지 않게 해주지는 않는다.