
| 리액트에서 렌더링 사이 값을 저장할 수 있게 해주는 훅으로 컴포넌트의 시각적 출력에 영향을 미치지 않는 정보를 저장하는데 적합하다.
useRef 의 몇 가지 특징을 살펴보면 다음과 같다.
state, props 가 변경되면 리렌더링이 일어나지만,useRef 훅을 사용하면 반응성 데이터(state, props) 값이 변경되어도 컴포넌트 리렌더링이 일어나지 않는다. ⇒ "렌더 트리거"가 없다.const ref = useRef(initialValue) 이며, 사용 형태는 다음과 같다.// 컴포넌트 최상위 레벨에서 호출하여 선언
import { useRef } from 'react'
export default function App() {
const countRef = useRef(1)
return (...
}
initialValue 는 초기 렌더링이후에는 무시된다.ref 객체 가 반환되는데, {current: 값} 형태이며 ref.current 로 값 읽기/수정 또는 접근할 수 있다.ref 를 가지며, 서로의 ref 객체를 공유하지는 않는다. (like 클래스 컴포넌트 인스턴스 멤버 작동)ref 를 가질 수 있다. (캐러셀 구현시 해당됨)💡 참고사항
- 사용자 정의 훅 내에서 리액트의 빌트인(내장) 훅 사용이 가능하다.
이런 특징들을 가진 useRef 훅 함수는 어떤 상황에서 사용하는 것이 효과적일까?
1️⃣ 이전 값을 어딘가 기억해둬야할 때 useRef 훅 함수가 빛을 발한다.
이전 값을 기억할 때, 다음 두 가지 사항을 참고해야한다.
이후 실행되는 이펙트 함수 를 사용하여 값을 참조해야한다.2️⃣ 타이머 기능이 예시로 가장 적합하지 않을까싶다.
타이머 기능 구현시, setInterval 함수가 반환한 타이머ID 값은 렌더링에 영향을 받지않도록 기억해둬야하는데 이럴 때 ref 객체를 생성하여 저장해둬야 한다.
import { useState, useRef } from 'react'
export default function App() {
const [timer, setTimer] = useState(null)
// useRef 훅함수 생성
const timerIdRef = useRef()
const handleStartTimer = () => {
;(function recursion() {
clearTimeout(timerIdRef.current)
timerIdRef.current = setTimeout(() => {
console.log('타이머 작동 중...')
setTimer(new Date())
recursion()
}, 1000)
})()
}
const handleStopTimer = () => {
console.log('타이머 중지!')
clearTimeout(timerIdRef.current)
setTimer(null)
}
return (
<section className="app">
<h1>타이머</h1>
<div role="group">
<button type="button" onClick={handleStartTimer}>
타이머 시작
</button>
<button type="button" onClick={handleStopTimer}>
타이머 중지
</button>
</div>
{timer && (
<time dateTime={timer.toISOString()}>
{timer.toLocaleTimeString()}
</time>
)}
</section>
)
}
클래스 컴포넌트는 자체적으로 상태관리가 가능하지만, 함수형 컴포넌트는리액트 훅을 사용하여 자체적으로 상태관리가 가능하다.
그렇다면 useRef 훅을 다뤄보는 이 시점에서 두 컴포넌트의 차이점에는 어떤 점이 있는지 살펴보고 넘어가자.
상태변경이다!)render 메서드를 재실행시킬 때마다 초기화가 이루어지기 때문에 이전 값을 기억할 수 없다.클래스 필드에 인스턴스 멤버로 생성을 하면 렌더링될 때마다 값을 기억하고 변경할 수 있다.트리거하려면 render() 내부에 setState 를 꼭 사용해야한다.state) 나 속성(props)이 아니더라도 특정한 값을 기억하는 것이 가능하다.this 는 고유한 메모리 공간을 가지므로, this를 통해 선언된 변수들은 인스턴스가 유지되는 동안 값을 저장할 수 있다)render 외부의 값을 기억할 수 있다. → 왜? render 의 외부일뿐, 클래스 컴포넌트의 내부니까!💡 리액트의 반응성 데이터에는 어떤 것들이 있을까?
- 상태 / 속성 / 컨텍스트 / 파생된 상태
지역변수는 렌더링될 때마다 값이 초기화되어 값을 기억할 수 없다.순수함이 사라지며,(함수형 컴포넌트 + 메모이제이션 공식을 이용하면 값 참조가 가능하다.리액트 훅 함수 이다.리액트는 값을 기억할 수 있게 만들었다! → useRef 를 이용해라!
렌더링은 대부분 리액트에 의해 제어되지만, 모든 경우를 제어하는 것은 불가능하다.
지금부터 다뤄볼 DOM조작 관련한 내용이 리액트가 제어할 수 없는 것들 중 하나이다.
실제 DOM 노드에 접근 또는 조작하는 것은 리액트의 렌더링과는 무관한 일이기도하고, 리액트가 렌더링 결과물에 맞춰 자동으로 DOM변경을 처리하기 때문에 컴포넌트에서 직접적으로 조작할 일이 드물긴하다.
그래도 가끔씩 DOM요소에 접근해야하는 경우들이 있는데, 대표적인 상황들은 다음과 같다.
useGSAP 을 제공하므로 훅 사용권장1️⃣ useRef 훅 가져오기
import { useRef } from 'react'
2️⃣ ref 선언을 위해 컴포넌트 내부에서 가져온 훅 사용하기
const myRef = useRef(null)
3️⃣ DOM노드를 가져와야하는 JSX tag 에 ref 를 attribute로 전달하기
<div ref={myRef}>
접근을 하고나면, 리액트가 해당(<div>) DOM노드를 생성할 때, 이 노드에 대한 참조를 초기값이 null 이였던 myRef.current 에 넣는다.
그럼 이제 이 DOM노드를 이벤트 핸들러에서 접근하거나 노드에 정의된 내장 브라우저 API를 사용할 수 있게 된다.
// 예시 - 아래와같이 브라우저 API를 사용할 수 있다.
myRef.current.scrollIntoView()
💬 Ref로 DOM 접근 및 조작하기 관련 참고내용
useRef는 리액트 앨리먼트가 실제 DOM에 마운트된 이후 참조할 수 있다.render 단계에서 DOM요소에 접근하려고하면 null 이 반환된다.useEffect 를 사용해야 할 수도 있다.useEffect 가 실행되기 때문이다.ref callback) 안에서 useEffect 실행도 가능하다.브라우저 요소를 출력하는 내장 컴포넌트(<input />)에 ref를 부여하면 리액트는 ref 의 current 프로퍼티를 브라우저의 실제 <input /> 과 같은 노드로 설정한다.
하지만, <MyInput /> 처럼 직접 만든 커스텀 컴포넌트에 ref를 부여하면 기본값으로 null 이 주어져 버튼을 클릭해도 input 요소에 focus 되지 않는다.
그렇다면 focus 되게하려면 어떻게 해야할까?
ref는 prop 처럼 부모 컴포넌트에서 자식 컴포넌트로 전달을 할 수가 있다.
import { useRef } from 'react';
function MyInput({ ref }) {
return <input ref={ref} />;
}
function MyForm() {
const inputRef = useRef(null);
return <MyInput ref={inputRef} />
}
위 예시에서 이를 적용시키면, 부모 컴포넌트인 MyForm 에서 ref를 생성하고 이를 자식 컴포넌트인 MyInput으로 전달한다. 그리고 MyInput 은 전달받은 ref를 반환하는 내장 컴포넌트인 <input> 에 전달하게 되므로 ref의 current 가 <input> DOM요소로 설정이 된다.
이 방식으로 진행하면 커스텀 컴포넌트에서도 ref를 사용할 수 있다.
💬 커스텀 컴포넌트 DOM노드 접근관련 참고내용
ref.current 는 null 인 상태이다.ref.current를 커밋 단계에서 설정한다.ref.current 를 재설정한다.ref 접근은 이벤트 핸들러 안에서 일어나는데, ref를 사용하여 특정 동작을 하고 싶지만 시행할 이벤트가 없을 땐 Effect 가 필요할 수 있다.함수 내부에서 특정한 값을 기억하고 싶은데, 렌더링은 유발하고싶지않을 때
useRef를 사용하면 좋다.
탈출구가 되는 것이다."리액트 규칙 글에서 살펴봤듯이 리액트 훅의 규칙에는 다음과 같은 내용이 있다.
최상단 에서만 호출되어야 한다.map()에서 호출할 수 없다. (이 글에서 map() 에서도 호출할 수 없음을 추가하겠다.)반복문이나 map() 에서 사용할 수 없는 상황을 해결하는 방법에 ref 콜백 이 있다.
ref callback 은 ref attribute 에 함수를 전달하는 것이다.
리액트19에서는 ref를 설정할때는 DOM노드와 함께 ref콜백을 호출하고, ref를 지울 때 ref콜백이 클린업함수(정리함수)를 반환하지 않으면, null로 호출된다고 했으나, 해당 동작은 향후 버전에서 제거될 예정인 점 참고하자.
Ref는 State처럼 숫자뿐만아니라 문자열, 객체, 함수 등 모든 것을 가리킬 수 있다.
또한, State와 마찬가지로 리렌더에 의해 유지된다.
하지만 State는 변경하면 컴포넌트 리렌더링이 발생하지만, Ref는 변경해도 리렌더링이 발생하지 않는다.
이렇듯 Ref 와 State 는 비슷한 면도 있지만 명확한 차이점이 있다.
이와 관련하여 리액트의 공식문서에 나와있는 Ref 와 State의 차이를 다음표를 통해 살펴보자.
| Ref | State |
|---|---|
useRef(initialValue) 는 { current: initialValue }를 반환한다. | useState(initialValue)는 State 변수의 현재 값과 Setter 함수 [value, setValue]를 반환한다. |
| State를 바꿔도 리렌더링 하지 않는다. | State를 바꾸면 리렌더링 한다. |
Mutable: 렌더링 프로세스 외부에서 current 값을 수정 및 업데이트 할 수 있다. | Immutable: State 수정을 위해서는 반드시 State설정 함수를 사용하여 리렌더링 대기열에 넣어야한다. |
렌더링 중에는 current 값을 읽거나 쓰면 안된다. | 언제든지 State 를 읽을 수 있지만, 렌더링마다 변경되지 않는 자체적인 State의 Snapshot 이 있다. |
🔗 참고자료
https://ko.react.dev/reference/react/useRef
https://ko.react.dev/learn/manipulating-the-dom-with-refs
https://ko.react.dev/learn/manipulating-the-dom-with-refs#how-to-manage-a-list-of-refs-using-a-ref-callback
https://ko.react.dev/learn/referencing-values-with-refs