💻 Frontend

2. React Hook, useRef

category
💻 Frontend
 
useRef는 함수형 컴포넌트에서 변수로 관리하거나 혹은 DOM에 접근할 수 있다.
 

1. 변수 관리

useState말고 useRef를 이용해 변수를 저장할 수 있다.
const ref = useRef(value) // {current: value}
current 값으로 접근하여 값을 바꿀 수 있다.
ref.current = 'hello'; // {current: 'hello'}
useRef랑 state, 일반 변수와의 차이점은 무엇일까? 먼저 state랑 비교해보자.
state와의 차이점
state는 값이 변경되면 state를 가지고 있는 함수는 렌더링 된다는 특징이 있다. 하지만 ref는 값이 변경되어도 렌더링 되지 않는다. 반대로 생각하여 어떠한 상황이 발생하여 렌더링이 발생해도 ref의 값은 변화되지 않는다.
  • state 변화 → 렌더링 O → 함수 내부 변수 초기화
  • state 변화 → 렌더링 O → ref 값은 유지됨
  • ref 변화 → 렌더링 X → 변수들의 값 유지됨
어떨 때 ref를 사용하면 좋을까?
이러한 특징들을 보아 자주 변경이 일어나는 변수는 ref로 관리해주는 것이 좋다. 왜냐하면 state로 관리하면 변화가 있을 때마다 렌더링을 하기 때문에 성능저하가 일어날 수 있기 때문이다.
변수와의 차이점
그렇다면 일반 변수와의 차이점은 무엇이 있을까? 위에서 봤듯이 일반 변수는 렌더링이 일어나면 값이 초기화가 된다. 하지만 ref는 렌더링이 일어났을 때 초기화가 되지 않는다. 아래 예제를 보며 이해를 해보자

예제

const useRefExample = () => { const [renderer, setRenderer] = useState(0); const [countState, setCountState] = useState(0); const countRef = useRef(0); let countVar = 0; console.log('렌더링...'); const IncreaseCountVar = () => { countVar += 1; console.log(countVar); }; const IncreaseCountState = () => { setCountState(countState + 1); console.log(countState); }; const IncreaseCountRef = () => { countRef.current += 1; console.log(countRef.current); }; const doRendering = () => { setRenderer(renderer + 1); }; const printResults = () => { console.log(`ref: ${countRef.current}, var: ${countVar}`); }; return ( <div> <p>Ref: {countRef.current}</p> <p>Var: {countVar}</p> <p>State: {countState}</p> <button onClick={doRendering}>렌더!</button> <button onClick={IncreaseCountState}>State 올려</button> <button onClick={IncreaseCountRef}>Ref 올려</button> <button onClick={IncreaseCountVar}>Var 올려</button> <button onClick={printResults}> Ref Var 값 출력</button> </div> ); }; export default useRefExample;
notion image
이렇게 화면이 보일 것이다.
1. Ref 올려 버튼을 눌렀을 때
ref 값은 증가 하였지만 화면에 Ref 숫자는 변경되지 않을 것이다. 왜냐하면 리렌더링이 일어나지 않았기 때문에 변경된 숫자가 반영이 안된 것이다.
1-2. 렌더! 버튼을 눌렀을 때
그 이후에 렌더! 버튼을 눌렀다면 그제서야 ref 값이 화면에도 변경된 것을 볼 수 있다.
 
2. Var 올려 버튼을 눌렀을 때
이 때도 마찬가지로 값은 증가했지만 숫자가 화면에 반영되지 않는다. 이유도 같다.
2-1. 렌더! 버튼을 눌렀을 때
렌더링을 시켜도 값이 반영이 안 된 것을 볼 수 있다. 왜냐하면 렌더링 됐을 때 변수가 초기화 됐기 때문이다.
 
3. State 올려 버튼을 눌렀을 때
값이 변경되고 화면에도 바로 반영이 된 것을 볼 수 있다. 콘솔창을 보면 값이 바뀔 때마다 렌더링이 일어난 것을 알 수 있다.
 

2. DOM 접근

ref의 유용한 점은 변수를 관리할 수 있지만 DOM에 직접 접근할 수 있다는 것이다. 자바스크립트 에서의 getElementById , querySelector 를 생각하면 된다.
const ref = useRef(value) <input ref={ref}> // {current: input } -> input의 모든 속성에 접근 가능

예제

어떤 특정한 상황이 왔을 때 input 박스에 자동으로 focus() 되는 기능을 만들어야 된다고 하자. 어떻게 input에 직접 접근하여 focus 메소드를 사용할 수 있을까?
이럴 때 useRef로 DOM에 접근하면 된다.
const useRefExample = () => { const inputRef = useRef(); useEffect(() => { inputRef.current.focus(); }, []); const login = () => { alert(`환영합니다 ${inputRef.current.value}`); inputRef.current.focus(); }; return ( <div> <input type="text" placeholder="username" ref={inputRef} /> <button onClick={login}>로그인</button> </div> ); }; export default useRefExample;
 
1. useRef를 선언해준다.
const inputRef = useRef();
2. 태그에 ref 속성을 추가하여 DOM 접근
<input type="text" placeholder="username" ref={inputRef} />
3. ref의 current값에 접근하여 focus 메소드 사용
useEffect(() => { inputRef.current.focus(); }, []);
 
++ input 안에 있는 값을 e.target.value로 불러오는 것이 아닌 ref 속성으로 inputRef.current.value로 불러올 수 있음
const login = () => { alert(`환영합니다 ${inputRef.current.value}`); };
 

공식문서 - useRef

useRef는 렌더링에 필요하지 않은 값을 참조할 수 있게 해주는 React Hook입니다.
 

문법

useRef(initialValue)

컴포넌트의 최상위 레벨에서 useRef를 호출하여 ref를 선언합니다.
import { useRef } from 'react'; function MyComponent() { const intervalRef = useRef(0); const inputRef = useRef(null); // ...
 

파라미터

  • initialValue: ref 객체의 current속성의 초기값입니다. 모든 유형의 값이 될 수 있습니다. 이 인수는 초기 렌더링 후에 무시됩니다.
 

반환값

useRef는 단일 속성을 가진 객체를 반환합니다:
  • current: 처음에는 전달한 initialValue로 설정됩니다. 나중엔 다른 것으로 설정할 수 있습니다. React에게 ref 객체를 JSX 노드에 대한 ref 속성으로 전달하면, current 속성을 설정합니다.
다음 렌더링에선 useRef는 동일한 객체를 반환합니다.
 

주의사항

  • ref.current 속성을 변경할 수 있습니다. state와 달리 변경 가능합니다. 그러나 렌더링에 사용되는 객체(예: state의 일부)를 보유하는 경우 해당 객체를 변경하면 안 됩니다.
  • ref.current 속성을 변경하면 React는 컴포넌트를 다시 렌더링하지 않습니다. ref는 일반 JavaScript 객체이기 때문에 React는 변경 시점을 인식하지 못합니다.
  • 초기화를 제외하고 렌더링 중에 ref.current를 쓰거나 읽지 마세요. 이로 인해 컴포넌트의 동작을 예측할 수 없게 됩니다.
  • Strict 모드에서 React는 우발적인 불순물을 찾는 데 도움을 주기 위해 컴포넌트 함수를 두 번 호출합니다. 이는 개발 전용 동작이며 프로덕션에는 영향을 주지 않습니다. 각 ref 개체는 두 번 생성되지만 두 개중 하나는 삭제됩니다. 구성 요소 함수가 순수한 경우(순수해야 함) 동작에 영향을 주지 않아야 합니다.
 

사용법

ref로 값 참조하기

컴포넌트의 최상위 레벨에서 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; }
나중에 해당 interval clear를 호출할 수 있도록 ref에서 해당 interval ID를 읽을 수 있습니다.
function handleStopClick() { const intervalId = intervalRef.current; clearInterval(intervalId); }
ref를 사용하면 다음을 확인할 수 있습니다:
  • 리렌더링 사이에 정보를 저장할 수 있습니다(렌더링할 때마다 리셋되는 일반 변수와 달리).
  • ref를 변경해도 다시 렌더링을 트리거하지 않습니다(다시 렌더링을 트리거하는 state 변수와 달리).
  • 정보는 구성 요소의 각 복사본에 로컬로 저장됩니다(외부 변수와 달리 공유됨).
ref를 변경해도 다시 렌더링되지 않으므로 ref는 화면에 표시하려는 정보를 저장하는 데 적합하지 않습니다. 대신 state를 사용하십시오. useRefuseState 중에서 선택하는 방법에 대해 자세히 알아보세요.
 
🚨
렌더링 중에 ref.current를 쓰거나 읽지 마십시오. React는 컴포넌트가 순수한 함수처럼 동작할 것으로 예상합니다.
  • 입력(props, state 및 context)이 동일한 경우 정확히 동일한 JSX를 반환해야 합니다.
  • 다른 순서로 호출하거나 다른 인수로 호출해도 다른 호출 결과에 영향을 미치지 않아야 합니다.
하지만 렌더링 중에 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>; }
대신 이벤트 핸들러 또는 effect에서 참조를 읽거나 쓸 수 있습니다.
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); } // ... }
렌더링 중에 무언가를 읽거나 써야 하는 경우 대신 state를 사용하십시오.
이러한 규칙을 어길 때 컴포넌트는 여전히 작동할 수 있지만 반응에 추가되는 대부분의 새로운 기능은 이러한 기대에 의존합니다. 컴포넌트를 순수하게 유지하는 방법에 대해 자세히 알아보십시오.
 

ref로 DOM 조작

ref를 사용하여 DOM을 조작하는 것은 일반적입니다. React는 이를 기본적으로 지원합니다.
먼저 초기 값nullref 객체를 선언합니다.
import { useRef } from 'react'; function MyComponent() { const inputRef = useRef(null); // ...
그런 다음 조작하려는 DOM 노드의 JSX에 ref 속성으로 ref 객체를 전달합니다.
// ... return <input ref={inputRef} />;
React가 DOM 노드를 생성하고 화면에 표시한 후 React는 ref 객체의 current 속성을 해당 DOM 노드로 설정합니다. 이제 <input>의 DOM 노드에 액세스하고 focus()와 같은 메서드를 호출할 수 있습니다.
function handleClick() { inputRef.current.focus(); }
React는 노드가 화면에서 제거되면 current 속성을 다시 null로 설정합니다.
ref로 DOM을 조작하는 방법에 대해 자세히 알아보세요.
 

ref 콘텐츠 재생성 방지

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를 쓰거나 읽을 수 없습니다. 그러나 이 경우에는 결과가 항상 동일하고 조건이 초기화 중에만 실행되므로 예측 가능하기 때문에 괜찮습니다.
 

문제 해결

커스텀 컴포넌트에 대한 ref를 가져올 수 없습니다.

다음과 같이 자신의 컴포넌트에 ref를 전달하려는 경우:
const inputRef = useRef(null); return <MyInput ref={inputRef} />;
콘솔에 오류가 표시될 수 있습니다.
🚨
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
기본적으로 자신의 컴포넌트는 내부 DOM 노드에 ref를 노출하지 않습니다.
이 문제를 해결하려면 ref를 얻으려는 컴포넌트를 찾으세요:
// before export default function MyInput({ value, onChange }) { return ( <input value={value} onChange={onChange} /> ); }
그런 다음 다음과 같이 forwardRef로 래핑합니다.
// after import { forwardRef } from 'react'; const MyInput = forwardRef(({ value, onChange }, ref) => { return ( <input value={value} onChange={onChange} ref={ref} /> ); }); export default MyInput;
그러면 해당 컴포넌트는 부모 컴포넌트에서 선언한 ref를 얻을 수 있습니다.
 

References