react - useRef를 활용해보자!
📌시작하며
useRef
를 언제 사용하나요? 라고 묻는다면, react에서 DOM 요소를 참조할 때 사용한다. 라고 답할 것이다! 실제로, useRef의 역할에는 DOM 요소를 참조하는 것이 있으니 틀린 말은 아니다.
하지만, 최근에 ‘최초 렌더링’ 상황에 스크롤이 최하단으로 내려가야 하는 컴포넌트를 구현하며, 자꾸만 해당 컴포넌트 내부의 것들이 렌더링 될 때마다 스크롤이 하단으로 내려가 난감하던 차에, useRef를 활용해 해결하면서, userRef가 단순히 DOM 요소를 참조하는 것 뿐만 아니라 다른 역할을 수행할 수 있음을 알게 되었다! 😎
그래서 이번에는 useRef의 활용법을 정리해보고자 한다.
🤓공식문서 설명 살펴보기
공식문서에서는 useRef 훅에 대해서
useRef는 렌더링에 필요하지 않은 값을 참조할 수 있는 React Hook입니다
라고 설명하고 있다.
📌매개변수
useRef는 useRef(initialValue)
과 같이 작성한다. 여기서 initialValue
는 ref 객체의 current프로퍼티의 초기 설정값으로 초기 렌더링 이후부터는 무시된다.
📌반환값
useRef는 current
라는 단일 프로퍼티 객체를 반환한다.
ref.current
프로퍼티를 변경해도 React는 컴포넌트를 다시 렌더링하지 않는다!- 즉, ref는 일반 JavaScript 객체이기 때문에 React는 사용자가 이 값을 언제 변경했는지 알지 못한다.
🎁값 참조하기
‘값’을 참조한다는 것에 state를 떠올리기 쉽지만, ref도 값을 참조할 수 있다! 여기서 중요한 차이점은, ref를 변경해도 리액트는 다시 렌더링 되지 않는다는 사실이다. 즉 ref는 컴포넌트의 시각적 출력에 영향을 미치지 않는 정보를 저장하는데 적합하다.
아래 예제는 state와 ref가 각각 어떤 차이점을 보이는지 알기 위한 예제다. useState로 카운트를 관리할 경우 count 버튼을 클릭할 때마다 값이 올라가는 것이 바로바로 시각적으로 보이지만, ref는 값이 올라감에도 불구하고 시각적으로 표현이 되지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
export default function Ref() {
const countRef = useRef<number>(0)
const incrementState = () => {
setCount(count + 1) // useState로 카운트를 증가
}
const incrementRef = () => {
countRef.current += 1
console.log("현재 ref의 값:", countRef.current)
}
return (
<Container>
<Container.Title>Ref 활용법</Container.Title>
<ul className="[&_h2]:font-medium [&_h2]:text-lg [&_h2]:mt-5">
<li>
<h2>3. ref와 state의 값 참조</h2>
<p className="text-gray-600 text-sm mb-3">
<code className="code">useState</code>는 값이 변경될때마다 리렌더링
시키지만, <code className="code">useRef</code>는 리렌더링 되지
않는다. 따라서, <code className="code">useRef</code>로 관리하고 있던
값이 증가됨에도 불가하고 실제 보이는 화면에서는 증가된 값이 보이지
않는다!
</p>
<div className="[&_button]:w-full [&_button]:py-2 [&_button]:bg-blue-500 [&_button]:rounded-md [&_button]:text-white [&_button]:hover:bg-blue-500/70 [&_button]:cursor-pointer">
<h1>useState 카운트: {count}</h1>
<button onClick={incrementState}>state로 관리되는 값 증가</button>
<h1>useRef 카운트: {countRef.current}</h1>
<button onClick={incrementRef}>ref로 관리되는 값 증가</button>
</div>
</li>
</ul>
</Container>
)
}
🎛️DOM 조작하기
일반적인 ref 사용방식이다. 예제는 두 input 중 한 input만 ref를 이용해 focus 하도록 했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
"use client"
import { useRef, useEffect, useState, RefObject } from "react"
import Container from "@/components/Container"
export default function RefDomExample() {
const inputRef = useRef<HTMLInputElement | null>(null)
const childRef = useRef<HTMLDivElement | null>(null)
const [childText, setChildText] = useState<string>(
"클릭해서 자식 컴포넌트의 text를 가져와볼까요?"
)
// 마운트 시 input에 포커스
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, [])
return (
<Container>
<Container.Title>Ref로 DOM 조작하기</Container.Title>
<ul className="[&_h2]:font-medium [&_h2]:text-lg [&_h2]:mt-5">
<li className="[&_input]:bg-gray-100 [&_input]:h-7 [&_input]:w-full [&_input]:rounded-md">
<h2>1. DOM 요소 참조하기</h2>
<p>
컴포넌트가 마운트되면, ref로 지정한 input이 자동으로 포커스가
맞춰집니다.
</p>
<p className="text-xs text-blue-500 mt-4 focus:border-blue-500">
1. ref를 사용해 DOM을 조작
</p>
<input ref={inputRef} />
<p className="text-xs text-red-500 mt-4">2. ref를 사용하지 않음</p>
<input />
</li>
</ul>
</Container>
)
}
🎇자식 요소 접근하기
해당 내용을 훑어보면서 함께 기록하면 좋을 것 같아 추가하자면, react 19부터는 forward ref가 사라지고 단순히 ref란 props를 이용해 자식요소에 접근할 수 있게되었다. 개인적으로 작성방식이 깔끔해져서 좋다!🙌
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
export default function Ref() {
const childRef = useRef<HTMLDivElement | null>(null)
const [childText, setChildText] = useState<string>(
"클릭해서 자식 컴포넌트의 text를 가져와볼까요?"
)
return (
<ul className="[&_h2]:font-medium [&_h2]:text-lg [&_h2]:mt-5">
<li>
<h2>2. 자식 컴포넌트의 DOM 접근하기</h2>
<p className="text-gray-600 text-sm mb-3">
참고로, react 19부터 <code className="code">forwardRef</code>가
사용되지 않으면서 <code className="code">ref</code> prop으로
변경되었다.
</p>
<div
onClick={() => {
setChildText(childRef.current?.innerText ?? "")
}}
>
<Child ref={childRef} />
</div>
<p>{childText}</p>
</li>
</ul>
)
}
const Child = ({ ref }: { ref: RefObject<HTMLDivElement | null> }) => {
return (
<div
ref={ref}
className="w-full h-12 bg-purple-600 text-white center-flex rounded-md cursor-pointer"
>
자식 컴포넌트 입니다!
</div>
)
}
🔒ref 콘텐츠 재생성 피하기
React는 초기에 ref 값을 한 번 저장하고 다음 렌더링에서는 이를 무시한다.
1
2
3
function Video() {
const playerRef = useRef(new VideoPlayer());
// ...
즉 위의 코드에서 초기 렌더링 시 new VideoPlayer()가 실행된 값이 ref에 저장된다! useRef는 리렌더링하더라도 값을 유지하므로, 이후 렌더링에서는 이 값이 변하지 않는다. 하지만, new VideoPlayer()
는 모든 렌더링에서 실행되며 불필요한 연산이 발생할 수 있다. 🤨
이를 막기 위해선 다음과 같은 방식을 사용할 수 있다.
1
2
3
4
5
6
7
function Video() {
const playerRef = useRef(null) // 초기값을 null로 설정
if (playerRef.current === null) {
playerRef.current = new VideoPlayer() // 처음 한 번만 실행됨!
}
}
초기값을 null로 설정하고, 조건식을 사용하면 오직 playerRef.current가 null일 때만 new VideoPlayer()를 실행하므로 이후의 렌더링이 발생하더라도 new VideoPlayer()
가 계속 재생성되지 않는다.