
컴포넌트가 일부 정보를 “기억”하기를 원하지만 해당 정보가 새 렌더링을 트리거하는 것을 원하지 않는 경우 참조를 ref를 사용할 수 있다.

리액트에서 useRef 훅을 가져와서 컴포넌트에 참조를 추가할 수 있다.
import { useRef } from 'react';
컴포넌트 내에서 useRef 훅을 호출하고 참조하려는 초기 값을 유일한 인수로 전달한다. 예를 들어, 다음은 값 0 에 대한 참조이다.
const ref = useRef(0);
useRef 는 다음과 같은 객체를 반환한다.
{
current: 0 // The value you passed to useRef
}
ref.current 속성을 통해 해당 참조의 현재 값에 액세스할 수 있다. 이 값은 의도적으로 변경가능하다. 즉, 읽고 쓸 수 있다. 이는 리액트가 추적하지 않는 컴포넌트의 비밀 주머니와 같다. (이것이 리액트의 단방향 데이터 흐름에서 “탈출구”를 만드는 이유이다.)
여기서 버튼을 클릭할 때마다 ref.current 를 증가시킨다.
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>
);
}
참조는 숫자를 가리키지만 상태와 마찬가지로 문자열, 객체, 함수 등 무엇이든 가리킬 수 있다. 상태와 달리 ref는 읽고 수정할 수 이쓴 current 속성이 있는 일반 JavaScript 객체이다.
컴포넌트는 모든 increment마다 리렌더링을 하지않는다. 상태와 마찬가지로 참조는 리렌더링 사이에 리액트에 의해 유지된다. 그러나 상태를 설정하면 컴포넌트가 리렌더링된다. ref는 바꾸는건 리렌더링되지 않는다.
단일 컴포넌트에서 참조와 상태를 결합할 수 있다. 예를 들어, 사용자가 버튼을 눌러 시작하거나 중지할 수 있는 스톱워치를 만들어 보겠다. 사용자가 “Start”를 누른 후 얼마나 많은 시간이 지났는지 표시하려면 시작 버튼을 누른 시간과 현재 시간을 추적해야 한다. 이 정보는 렌더링에 사용되므로 다음과 같은 상태로 유지된다.
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
사용자가 “Start”를 누르면 10밀리초마다 시간을 업데이트하기 위해 [setInterval](https://developer.mozilla.org/docs/Web/API/setInterval) 을 사용한다.
import { useState } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
function handleStart() {
// Start counting.
setStartTime(Date.now());
setNow(Date.now());
setInterval(() => {
// Update the current time every 10ms.
setNow(Date.now());
}, 10);
}
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>
</>
);
}
“Stop” 버튼을 누르면 now 상태 변수 업데이트가 중단 되도록 기존 interval를 취소해야 한다. [clearInterval](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval) 을 호출하여 이 작업을 수행할 수 있지만 사용자가 시작을 눌렀을 때 이전에 setInterval 호출에 의해 반환된 interval ID를 제공해야한다. interval ID를 어딘가 보관해야 한다. interval ID는 렌더링에 사용되지 않으므로 참조에 보관할 수 있다.
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를 사용하는 것이 더 효율적일 수 있다.
아마도 너는 ref가 state보다 덜 “엄격”하다고 생각할 것이다. 예를 들어 항상 상태 설정 함수를 사용하는 대신 ref를 변경할 수 있다. 하지만 대부분의 경우 상태를 사용하고 싶을 것이다. Refs는 자주 필요하지 않은 “탈출용 해치”이다. 상태와 참조를 비교하는 방법은 다음과 같다.
| refs | state |
|---|---|
| useRef(initialValue) 는 { current: initialValue } 를 반환한다. | useState(initialValue) 는 상태 변수의 현재 값과 상태 설정 함수( [value, setValue] ) 반환 |
| 변경할 때 리렌더링되지 않는다. | 변경하면 리렌더링된다. |
| 변경 가능 - 렌더링 프로세스 외부에서 current 값을 수정하고 업데이트할 수 있다. | “불변” - 리렌더링을 대기역에 추가하려면 상태 설정 함수를 사용하여 상태 변수를 수정해야한다. |
| 렌더링하는 동안 current 값을 읽거나 쓰면 안된다. | 언제든지 상태를 읽을 수 있다. 그러나 각 렌더링에는 변경되지 않는 자체 사태 스냅샷이 있다. |
다음은 상태로 구현된 카운터 버튼이다.
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
You clicked {count} times
</button>
);
}
count 값이 표시되므로 이에 대한 상태 값을 사용하는 것이 합리적이다. setCount() 를 사용하여 카운터 값을 설정하면 리액트는 컴포넌트를 리렌더링하고 화면은 새 카운트를 반영하도록 업데이트된다.
이를 참조로 구현하려고 하면 리액트는 컴포넌트를 리렌더링하지 않으므로 개수 변경을 볼 수 없다. 이 버튼을 클릭해도 텍스트가 어떻게 업데이트되지 않는지 확인해라.
import { useRef } from 'react';
export default function Counter() {
let countRef = useRef(0);
function handleClick() {
// This doesn't re-render the component!
countRef.current = countRef.current + 1;
}
return (
<button onClick={handleClick}>
You clicked {countRef.current} times
</button>
);
}
이것이 렌더링 중에 ref.current 를 읽는 것이 신뢰할 수 없는 코드로 이어지는 이유이다. 필요한 경우 대신 상태를 사용해라.
How does useRef work inside?
useState 와 useRef 는 모두 리액트에서 제공되지만 원칙적으로 useRef 는 useState 위에 구현될 수 있다. 리액트 내부에서 useRef 가 다음과 같이 구현되어 있다고 상상할 수 있다.
// Inside of React
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}
첫 번째 렌더링 중에 useRef 는 { current: initialValue } 를 반환한다. 이 객체는 리액트에 의해 저장되므로 다음 렌더링 중에 동일한 객체가 반환된다. 이 예에서는 상태 설정 함수가 어떻게 사용되지 않는지 확인해라. useRef 는 항상 동일한 객체를 반환해야 하기 때분에 불필요하다.
리액트는 실제로 충분히 일반적이기 때문에 내장 버전의 useRef 를 제공한다. 하지만 setter가 없는 일반 상태 변수로 생각할 수 있다. 객체 지향 프로그래밍에 익숙하다면 refs가 인스턴스 필드를 생각나게 할 수 있다. 하지만 this.something 대신 somethingRef.current 를 작성한다.
일반적으로 컴포넌트가 리액트 외부로 나가서 외부 API( 종종 컴포넌의 모양에 영향을 주지 않는 브라우저 API )와 통실해야 할 때 참조를 사용한다. 다음은 이러한 드문 상황 중 일부이다.
컴포넌트가 일부 값을 저장해야 하지만 렌더링 로직에 영향을 주지 않는 경우 refs를 선택해라.
다음 원칙을 따르면 컴포넌트를 더욱 예측하기 쉽게 만들 수 있다.
ref.current 를 읽거나 쓰지 마라. 렌더링 중에 일부 정보가 필요한 경우 상태를 사용해라. 리액트는 ref.current 가 언제 변경되는지 모르기 때문에 렌더링하는 동안 이를 읽어도 컴포넌트의 동작을 예측하기 어렵다. ( 유일한 예외는 첫 번째 렌더링 중에 참조를 한번만 설정 하는 if (!ref.current) ref.current = new Thing() 과 같은 코드이다.리액트 상태의 제한 사항은 ref에 적용되지 않는다. 예를 들어 상태는 모든 렌더링에 대한 스냅샷처럼 작동하며 동기적으로 업데이트되지 않는다. 그러나 참조의 현재 값을 변경하면 즉시 변경된다.
ref.current = 5;
console.log(ref.current); // 5
이는 참조 자체가 일반 JavaScript 객체이므로 참조 객체처럼 동작하기 때문이다.
또한 ref로 작업할 때 mutation을 피하는 것에 대해 걱정할 필요가 없다. 변경하려는 객체가 렌더링에 사용되지 않는 한 리액트는 참조나 그 내용으로 무엇을 하든 상관하지 않는다.
참조는 어떤 값이든 가리킬 수 있다. 그러나 ref의 가장 일반적인 사용 사례는 DOM 요소를 액세스 하는 것이다. 예를 들어 프로그래밍 방식으로 입력에 집중하려는 경우에 유용하다. <div ref={myRef}> 와 같이 JSX의 ref 속성에 ref를 전달하면 리액트는 해당 DOM 요소를 myRef.current 에 넣는다. 요소가 DOM에서 제거되면 리액트는 myRef.current 를 null 로 업데이트한다. 이에 대한 자세한 내용은 Manipulating the DOM with Refs. 에서 읽을 수 있다.
current 라는 단일 속성이 있는 일반 JavaScript 객체이다.useRef 훅을 호출하여 리액트에게 참조를 제공하도록 요청할 수 있다.current 값을 설정해도 리렌더링이 트리거되지 않는다.ref.current 를 읽거나 쓰지 마라. 이로 인해 컴포넌트를 예측하기가 어려워진다.