useRef는 렌더링에 필요하지 않은 값을 참조할 수 있게 해주는 React Hook이에요.
const ref = useRef(initialValue)
useRef(initialValue) {#useref}컴포넌트의 최상위 레벨에서 useRef를 호출해서 ref를 선언하세요.
import { useRef } from 'react';
function MyComponent() {
const intervalRef = useRef(0);
const inputRef = useRef(null);
// ...
initialValue: ref 객체의 current 프로퍼티가 처음에 가질 값이에요. 어떤 타입의 값이든 넣을 수 있어요. 이 인자는 초기 렌더링 이후에는 무시돼요.useRef는 하나의 프로퍼티를 가진 객체를 반환해요:
current: 처음에는 여러분이 전달한 initialValue로 설정돼요. 나중에 다른 값으로 바꿀 수 있어요. ref 객체를 JSX 노드의 ref 속성으로 React에 전달하면, React가 current 프로퍼티를 해당 DOM 노드로 설정해줘요.다음 렌더링에서도 useRef는 동일한 객체를 반환해요.
ref.current 프로퍼티는 변경할 수 있어요(mutate 가능). state와 달리 mutable(변경 가능)하거든요. 하지만 렌더링에 사용되는 객체(예를 들어 state의 일부)를 담고 있다면, 그 객체를 직접 변경하면 안 돼요.ref.current 프로퍼티를 변경해도 React는 컴포넌트를 다시 렌더링하지 않아요. ref는 그냥 평범한 JavaScript 객체이기 때문에, 여러분이 그걸 변경해도 React는 알 수가 없어요.ref.current를 쓰거나 읽지 마세요. 이렇게 하면 컴포넌트의 동작을 예측할 수 없게 돼요.컴포넌트의 최상위 레벨에서 useRef를 호출해서 하나 이상의 ref를 선언하세요.
import { useRef } from 'react';
function Stopwatch() {
const intervalRef = useRef(0);
// ...
useRef는 여러분이 제공한 초기값으로 설정된 하나의 current 프로퍼티를 가진 ref 객체를 반환해요.
다음 렌더링에서도 useRef는 동일한 객체를 반환해요. current 프로퍼티를 변경해서 정보를 저장하고 나중에 읽을 수 있어요. 이게 state를 떠올리게 할 수도 있는데, 중요한 차이점이 있어요.
ref를 변경해도 리렌더링이 발생하지 않아요. 이 말은 ref가 컴포넌트의 시각적 출력에 영향을 주지 않는 정보를 저장하는 데 딱 맞다는 뜻이에요. 예를 들어, interval ID를 저장해두었다가 나중에 가져와야 할 때 ref에 넣어둘 수 있어요. ref 안의 값을 업데이트하려면, current 프로퍼티를 직접 변경해야 해요:
function handleStartClick() {
const intervalId = setInterval(() => {
// ...
}, 1000);
intervalRef.current = intervalId;
}
나중에 ref에서 그 interval ID를 읽어서 해당 interval을 정리할 수 있어요:
function handleStopClick() {
const intervalId = intervalRef.current;
clearInterval(intervalId);
}
ref를 사용하면 다음을 보장할 수 있어요:
ref를 변경해도 리렌더링이 트리거되지 않으니까, 화면에 표시하고 싶은 정보를 저장하는 데는 ref가 적합하지 않아요. 그런 경우에는 state를 대신 사용하세요. useRef와 useState 중에 선택하는 방법에 대해 더 읽어보세요.
이 컴포넌트는 버튼이 몇 번 클릭되었는지 추적하기 위해 ref를 사용해요. 여기서 state 대신 ref를 사용하는 게 괜찮은 이유는 클릭 횟수가 이벤트 핸들러에서만 읽히고 쓰이기 때문이에요.
import { useRef } from 'react';
export default function Counter() {
let ref = useRef(0);
function handleClick() {
ref.current = ref.current + 1;
alert('You clicked ' + ref.current + ' times!');
}
return (
<button onClick={handleClick}>
Click me!
</button>
);
}
만약 JSX에서 {ref.current}를 보여주면, 클릭해도 숫자가 업데이트되지 않을 거예요. ref.current를 설정하는 것은 리렌더링을 트리거하지 않으니까요. 렌더링에 사용되는 정보는 state로 관리해야 해요.
이 예제는 state와 ref의 조합을 사용해요. startTime과 now는 둘 다 렌더링에 사용되기 때문에 state 변수예요. 하지만 버튼을 누를 때 interval을 멈추기 위해 interval ID도 가지고 있어야 해요. interval ID는 렌더링에 사용되지 않으니까, ref에 보관하고 직접 업데이트하는 게 적절해요.
import { useState, useRef } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
const intervalRef = useRef(null);
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
function handleStop() {
clearInterval(intervalRef.current);
}
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
Start
</button>
<button onClick={handleStop}>
Stop
</button>
</>
);
}
⚠️ 주의: 렌더링 중에
ref.current를 쓰거나 읽지 마세요.React는 컴포넌트의 본문이 순수 함수처럼 동작하기를 기대해요:
렌더링 중에 ref를 읽거나 쓰면 이런 기대를 깨뜨리게 돼요.
function MyComponent() { // ... // 🚩 렌더링 중에 ref를 쓰지 마세요 myRef.current = 123; // ... // 🚩 렌더링 중에 ref를 읽지 마세요 return <h1>{myOtherRef.current}</h1>; }대신 이벤트 핸들러나 effect에서 ref를 읽거나 쓸 수 있어요.
function MyComponent() { // ... useEffect(() => { // ✅ effect에서 ref를 읽거나 쓸 수 있어요 myRef.current = 123; }); // ... function handleClick() { // ✅ 이벤트 핸들러에서 ref를 읽거나 쓸 수 있어요 doSomething(myOtherRef.current); } // ... }만약 렌더링 중에 무언가를 꼭 읽거나 써야 한다면, 대신 state를 사용하세요.
이 규칙을 어기더라도 컴포넌트가 여전히 작동할 수도 있지만, React에 추가하고 있는 대부분의 새로운 기능들은 이런 기대에 의존하고 있어요. 컴포넌트를 순수하게 유지하는 것에 대해 더 읽어보세요.
ref를 사용해서 DOM을 조작하는 건 특히 흔한 패턴이에요. React는 이걸 기본적으로 지원하고 있어요.
먼저, 초기값을 null로 해서 ref 객체를 선언하세요:
import { useRef } from 'react';
function MyComponent() {
const inputRef = useRef(null);
// ...
그다음 여러분의 ref 객체를 조작하고 싶은 DOM 노드의 JSX에 ref 속성으로 전달하세요:
// ...
return <input ref={inputRef} />;
React가 DOM 노드를 생성하고 화면에 넣은 후, React는 ref 객체의 current 프로퍼티를 그 DOM 노드로 설정해줘요. 이제 <input>의 DOM 노드에 접근해서 focus() 같은 메서드를 호출할 수 있어요:
function handleClick() {
inputRef.current.focus();
}
노드가 화면에서 제거되면 React는 current 프로퍼티를 다시 null로 설정해줘요.
ref로 DOM 조작하기에 대해 더 읽어보세요.
이 예제에서는 버튼을 클릭하면 input에 포커스가 가요:
import { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
이 예제에서는 버튼을 클릭하면 이미지가 뷰로 스크롤돼요. 리스트 DOM 노드에 대한 ref를 사용하고, 그 다음 DOM querySelectorAll API를 호출해서 스크롤하고 싶은 이미지를 찾아요.
import { useRef } from 'react';
export default function CatFriends() {
const listRef = useRef(null);
function scrollToIndex(index) {
const listNode = listRef.current;
// This line assumes a particular DOM structure:
const imgNode = listNode.querySelectorAll('li > img')[index];
imgNode.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
return (
<>
<nav>
<button onClick={() => scrollToIndex(0)}>
Neo
</button>
<button onClick={() => scrollToIndex(1)}>
Millie
</button>
<button onClick={() => scrollToIndex(2)}>
Bella
</button>
</nav>
<div>
<ul ref={listRef}>
<li>
<img
src="https://placecats.com/neo/300/200"
alt="Neo"
/>
</li>
<li>
<img
src="https://placecats.com/millie/200/200"
alt="Millie"
/>
</li>
<li>
<img
src="https://placecats.com/bella/199/200"
alt="Bella"
/>
</li>
</ul>
</div>
</>
);
}
div {
width: 100%;
overflow: hidden;
}
nav {
text-align: center;
}
button {
margin: .25rem;
}
ul,
li {
list-style: none;
white-space: nowrap;
}
li {
display: inline;
padding: 0.5rem;
}
이 예제는 <video> DOM 노드에서 play()와 pause()를 호출하기 위해 ref를 사용해요.
import { useState, useRef } from 'react';
export default function VideoPlayer() {
const [isPlaying, setIsPlaying] = useState(false);
const ref = useRef(null);
function handleClick() {
const nextIsPlaying = !isPlaying;
setIsPlaying(nextIsPlaying);
if (nextIsPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}
return (
<>
<button onClick={handleClick}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<video
width="250"
ref={ref}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
>
<source
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
type="video/mp4"
/>
</video>
</>
);
}
button { display: block; margin-bottom: 20px; }
가끔은 부모 컴포넌트가 여러분의 컴포넌트 안의 DOM을 조작할 수 있게 하고 싶을 수 있어요. 예를 들어, MyInput 컴포넌트를 만들고 있는데 부모가 input에 포커스를 줄 수 있게 하고 싶다면요(부모는 해당 input에 직접 접근할 수 없으니까요). 부모에서 ref를 만들고 자식 컴포넌트에 ref를 prop으로 전달하면 돼요. 자세한 설명은 여기서 읽어보세요.
import { useRef } from 'react';
function MyInput({ ref }) {
return <input ref={ref} />;
};
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
React는 초기 ref 값을 한 번 저장하고 다음 렌더링에서는 이걸 무시해요.
function Video() {
const playerRef = useRef(new VideoPlayer());
// ...
new VideoPlayer()의 결과는 초기 렌더링에서만 사용되지만, 매 렌더링마다 이 함수를 호출하고 있긴 해요. 비싼 객체를 생성하는 거라면 이건 낭비가 될 수 있어요.
이걸 해결하려면, 대신 이렇게 ref를 초기화할 수 있어요:
function Video() {
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
// ...
보통 렌더링 중에 ref.current를 쓰거나 읽는 건 허용되지 않아요. 하지만 이 경우에는 괜찮아요. 왜냐하면 결과가 항상 같고, 조건문이 초기화 중에만 실행되기 때문에 완전히 예측 가능하거든요.
심화: useRef를 나중에 초기화할 때 null 체크를 피하는 방법 {#how-to-avoid-null-checks-when-initializing-use-ref-later}
타입 체커를 사용하는데 항상
null체크를 하고 싶지 않다면, 이런 패턴을 시도해볼 수 있어요:function Video() { const playerRef = useRef(null); function getPlayer() { if (playerRef.current !== null) { return playerRef.current; } const player = new VideoPlayer(); playerRef.current = player; return player; } // ...여기서
playerRef자체는 nullable이에요. 하지만getPlayer()가null을 반환하는 경우는 없다는 걸 타입 체커에게 납득시킬 수 있을 거예요. 그러면 이벤트 핸들러에서getPlayer()를 사용하면 돼요.
여러분의 컴포넌트에 이렇게 ref를 전달하려고 하면:
const inputRef = useRef(null);
return <MyInput ref={inputRef} />;
콘솔에 이런 에러가 나올 수 있어요:
❌ TypeError: Cannot read properties of null
기본적으로 여러분이 만든 컴포넌트는 안에 있는 DOM 노드에 대한 ref를 노출하지 않아요.
이걸 해결하려면, ref를 전달하고 싶은 컴포넌트를 찾으세요:
export default function MyInput({ value, onChange }) {
return (
<input
value={value}
onChange={onChange}
/>
);
}
그리고 컴포넌트가 받는 props 목록에 ref를 추가하고, 관련 자식 빌트인 컴포넌트에 ref를 prop으로 전달하세요. 이렇게요:
function MyInput({ value, onChange, ref }) {
return (
<input
value={value}
onChange={onChange}
ref={ref}
/>
);
};
export default MyInput;
그러면 부모 컴포넌트가 ref를 가져올 수 있어요.
다른 컴포넌트의 DOM 노드에 접근하기에 대해 더 읽어보세요.