useRef란 렌더링에 필요하지 않은 값을 참조할 수 있는 React의 훅이다.
일반적인 변수의 경우 리렌더링이 일어날 때마다 초기화가 이루어지는 것과 달리, ref의 경우에는 리렌더링 사이에 정보를 저장할 수 있다.
import { useRef } from 'react';
function MyComponent() {
const intervalRef = useRef(0);
const inputRef = useRef(null);
// ...
useRef(initialValue)
initialValue : ref 객체의 current 프로퍼티 초기 설정값이다. 이 인자는 초기 렌더링 이후부터는 무시된다.useRef는 단일 프로퍼티 current를 가진 객체를 반환한다.
current : 처음에 전달한 initialValue로 초기화된다. 나중에 다른 값으로 변경할 수 있다.ref.current 속성은 state와 달리 변이할 수 있다. 그러나 렌더링에 사용되는 객체를 포함하는 경우 해당 객체를 변이해서는 안된다.
💡 변이?
const [position, setPosition] = useState({ x: 0, y: 0 }); position.x = 5; // 변이객체 자체의 내용을 변경하는 것. React의
state의 객체는 기술적으로는 변이를 할 수 있지만 number, boolean, string 처럼 불변하는 것처럼 취급해야 한다.
객체를 직접 변이하는 대신, set을 사용해 교체해야 한다.
또한 ref.current 속성을 변경해도 React 컴포넌트 리렌더링이 발생하지 않는다. ref는 일반 JavaScript 객체이기 때문에 React는 변경 감지를 하지 못한다.
또한 초기화를 제외하고는 렌더링 중에 ref.current를 읽거나 쓰지 말아야 한다. 이렇게 하면 컴포넌트의 동작을 예측할 수 없기 때문이다. React는 컴포넌트가 순수 함수처럼 동작하길 기대한다. 즉, 입력인props, state, context가 동일하면 동일한 결과(렌더링 결과)가 반환되기를 기대한다.
그렇지만 렌더링 중에 ref를 읽거나 쓰면 이런 기대가 깨진다.
function MyComponent() {
// ...
// 🚩 Don't write a ref during rendering
myRef.current = 123;
// ...
// 🚩 Don't read a ref during rendering
return <h1>{myOtherRef.current}</h1>;
}
대신에, 이벤트 핸들러나 useEffect에서 ref를 읽거나 쓸 수 있다.
function MyComponent() {
// ...
useEffect(() => {
// ✅ You can read or write refs in effects
myRef.current = 123;
});
// ...
function handleClick() {
// ✅ You can read or write refs in event handlers
doSomething(myOtherRef.current);
}
// ...
}
@types/react의 index.d.ts를 보면 useRef는 3개의 정의가 오버로딩 되어있다.
function useRef<T>(initialValue: T): MutableRefObject<T>
function useRef<T>(initialValue: T | null): RefObject<T>
function useRef<T = undefined>(): MutableRefObject<T | undefined>
interface MutableRefObject<T> {
current: T
}
interface RefObject<T> {
readonly current: T | null
}
useRef의 반환 타입을 보면 RefObject 와 MutableRefObject가 있다. 각각 타입 정의를 보면 current 속성을 갖는 것을 알 수 있다.
매개변수 타입과 제네릭 타입이 T로 일치한다면 MutableRefObject 타입을 반환한다.
const localVarRef = useRef<number>(0)
매개변수 타입이 null을 허용한다면 RefObject 타입을 반환한다.
const localVarRef = useRef<number>(null)
위에서는 매개변수로 null을 전달했는데, 다시 한번 오버로딩을 보자.
function useRef<T>(initialValue: T | null): RefObject<T>
interface RefObject<T> {
readonly current: T | null
}
null 값을 전달하는 경우 RefObject 타입을 반환하는데, 이 때 current 속성이 readonly 이므로 나중에 값을 수정할 수 없다. 그렇지만 대상은 current 속성이므로, current 하위의 속성들은 얼마든지 수정이 가능하다.
input 컴포넌트의 focus를 자동 설정하는 가장 일반적인 코드를 생각해보자.
const Input = () => {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
ref.current?.focus();
}, []);
return <input ref={ref} />;
};
렌더링을 마쳐서 input 컴포넌트가 mount 된 후 useEffect 훅이 호출되기 때문에, ref.current 에서 DOM 요소에 접근할 수 있다.
그렇다면 아래의 경우는 어떨까?
const Input = () => {
const [show, setShow] = useState(false);
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
ref.current?.focus();
}, []);
return (
<>
<button onClick={() => setShow(true)}>show input</button>
{show && <input ref={ref} />}
</>
);
};
이 경우에는 렌더링을 모두 마쳐도 input이 mount 되지 않았기 때문에 useEffect의ref.current에서 DOM 요소에 접근이 불가능하다. 물론 useEffect의 의존성 배열에 show 를 추가한다면 ref.current에서 DOM 요소에 접근이 가능하다.
useEffect(() => {
ref.current?.focus();
}, [show]);
그렇지만 ref를 상위 컴포넌트에서 전달받을 때는 이러한 것도 불가능하다.
function App() {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
ref.current?.focus(); // 어떻게 상위 컴포넌트에서 input 컴포넌트 mount를 감지?
}, []);
return (
<div>
<Input ref={ref} />
</div>
);
}
const Input = forwardRef((_, ref: ForwardedRef<HTMLInputElement>) => {
const [show, setShow] = useState(false);
return (
<>
<button onClick={() => setShow(true)}>show input</button>
{show && <input ref={ref} />}
</>
);
});
ForwardedRef<T> 타입을 자세히 보면 ((instance: T | null)) ⇒ void 라는 함수 타입이 포함되어 있는것을 알 수 있다.
type ForwardedRef<T> =
((instance: T | null) => void) | React.MutableRefObject<T | null> | null
이 말은 우리가 props로 ref를 전달하는 것이 함수를 전달하는 것을 대신 해주는 것과 동일하다는 의미이다.
<input ref={ref} />
<input ref={(node) => ref.current = node} />
React Element의 ref는 컴포넌트가 렌더링 된 이후 실행되는 함수라고 생각하면 된다. 이 함수는 렌더링 된 DOM node를 인자로 받고, 해당 컴포넌트가 unmount 되면 한번 더 호출되어 null을 인자로 받는다.
그러니까 아까 전 예제에서 굳이 ref 객체를 전달할 필요 없이, 함수를 전달하면 컴포넌트가 렌더링 될 때마다 실행되므로 input이 mount가 되었을 때 focus() 메소드를 호출할 수 있게 된다.
function App() {
return (
<div>
<Input ref={(node) => node?.focus()} />
</div>
);
}
이 때, 위와 같이 ref를 전달하면 매 렌더링마다 함수가 생성되므로 useCallback을 사용해서 함수를 감싸주면 렌더링 최적화를 진행해줄 수 있다. 이것이 Callback Ref라고 불리는 이유이다.
function App() {
const focus = useCallback((node: HTMLElement | null) => node?.focus(), []);
return (
<div>
<Input ref={focus} />
</div>
);
}