React hook 알아보기 세 번째 hook은 useRef
이다.
많이 사용해보진 않았지만 내가 알고 있던 이 hook의 특징은 DOM
요소에 접근해야할 때 사용하는 것으로만 알고 있지만 이번에 한 번 제대로 알아보고 가보자!
DOM
노드 액세스먼저 DOM
노드에 접근하려면 구성 요소 내부에 ref를 선언 후 DOM
노드에 ref
속성으로 전달한다.
import {useRef} from "react";
function MyComponent(props) {
const myRef = useRef(null);
return <div ref={myRef} />
}
useRef
는 current
라는 단일 속성을 가진 객체를 반환한다. myRef.current
의 초기값은 null
이며, 리액트가 <div>
에 대한 DOM
노드를 생성하면 리액트는 이 노드에 대한 참조를 myRef.current
에 작성하게 된다.
그리고나서 이 노드에 대해 내장 브라우저 API를 사용할 수 있게 된다.
myRef.current.focus();
useRef
를 사용한 대표적인 예import {useRef} from 'react';
function MyInputComponent(props) {
const inputRef = useRef(null);
return (
<div>
<input ref={inputRef} />
<button onClick={()=> inputRef.current.focus();}>
Focus input
</button>
</div>
);
}
이 예시의 코드를 작성해서 화면에 있는 버튼을 눌러보면 input
요소에 focus
되는 것을 확인할 수 있다. 이처럼 DOM
조작은 useRef
의 가장 일반적인 사용 사례이다.
또한 scroll 구현 시 원하는 노드의 위치로 이동할 수 있는 버튼을 만든다거나, 화면을 이동할 때 DOM
노드를 감지하여 액션을 하는 등에 사용할 수 있을 것이다.
function MyComponent(props) {
const [visible, setVisible] = useState(false);
const pageRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => { // IntersectionObserverEntry의 속성이 담긴 배열
if (!entry.isIntersecting) { // 관찰 대상이 화면과 교차 상태에서 나갈 때
setVisible(false);
return;
}
setVisible(entry.isIntersecting);// 관찰 대상이 화면과 교차 상태일 때
},
{
threshold: 0.9,
// 옵저버가 실행되기 위해 타겟의 가시성이 얼마나 필요한지의 값
// 0.9 => 90% 보여져야 옵저버가 실행
}
);
if (ref.current) {
observer.observe(ref.current);
return;
}
return () => {
if (ref.current) { // 화면을 벗어날 때 opserver 제거
observer.unobserve(ref.current);
}
};
}, [ref]);
}
return (
<div ref={pageRef}>
{visible === true ? children : null}
</div>
)
이런식으로 useRef
와 사용자 화면에 지금 보이는 요소인지 아닌지 구별하는 기능인 IntersectionObserver
를 사용하여 코드를 작성했다.
두 화면의 차이점은 내가 처음 페이지에 접근하였을 때는 동일하게 애니메이션이 발생하는데 다른 화면에 넘어갔다 돌아오면 애니메이션이 다시 재생되도록 한 부분이다. 이런 식으로 화면을 이동할 때 IntersectionObserve
로 DOM
노드를 관찰하여 애니메이션을 다시 재생하도록 할 수 있을 것이다.
리액트 컴포넌트 함수의 경우 내부의 State
가 변할 때마다 리액트 컴포넌트 함수가 호출되어 화면이 갱신된다. 하지만 리렌더링으로 인해 함수 내의 값이 초기화가 된다. 가끔은 리렌더링이 되어도 기존에 참조하고 있던 컴포넌트 함수 내의 값이 그대로 보존되어야 할 때가 있다. 이런 경우에 useRef
의 current
속성을 이용하여 처리한다.
const ref = useRef(0);
console.log(ref);
// { current: 0 } -> ref의 반환 값
useRef.current
속성을 통해 참조의 초기 값에 액세스 할 수 있으며 이 값은 변경이 가능하고, 읽고 사용할 수 있다.
그럼 이 useRef
를 사용하여 참조 값을 유지하는 방법을 알아보기 전에 어떤 문제가 있기에 useRef
를 사용하여야 하는지 문제의 코드를 보고 넘어가자.
function ManualCounter() {
const [count, setCount] = useState(0);
let intervalId;
const startCounter = () => {
// 💥 매번 새로운 값 할당
intervalId = setInterval(() => setCount((count) => count + 1), 1000);
};
const stopCounter = () => {
clearInterval(intervalId);
};
return (
<>
<p>자동 카운트: {count}</p>
<button onClick={startCounter}>시작</button>
<button onClick={stopCounter}>정지</button>
</>
);
}
이 코드의 문제는 interval
을 위해 선언 된 intervalId
변수이다. 이 변수를 startCounter()
함수와 stopCounter()
함수에 공유해야 하기에 함수 밖에서 선언해야 한다. 이렇게 되면 count
의 상태 값이 바뀔 때마다 컴포넌트 함수가 호출되어 리렌더링 되고, intervalId
의 값도 매번 변경될 것이다. 이렇게 되면 브라우저 메모리에서 정리하지 못한 intervalId
가 쌓이게된다.
useRef
사용하기function ManualCounter() {
const [count, setCount] = useState(0);
const intervalId = useRef(null);
const startCounter = () => {
intervalId.current = setInterval(
() => setCount((count) => count + 1),
1000
);
};
const stopCounter = () => {
clearInterval(intervalId.current);
};
return (
<>
<p>자동 카운트: {count}</p>
<button onClick={startCounter}>시작</button>
<button onClick={stopCounter}>정지</button>
</>
);
}
useRef
를 사용하여 코드를 개선해보았다. current
속성은 값을 변경해도 상태를 변경할 때 처럼 리액트 컴포넌트가 다시 렌더링되지 않는다. 또한 리액트 컴포넌트가 리렌더링 될 때에도 마찬가지로 current
속성의 값은 참조 값을 유지하고 있다.
이 코드를 실행하여 시작, 정지를 눌러보면 카운트가 1초마다 올라가서 count
가 증가하는데, 정지와 시작이 눌릴 때 값은 마지막에 참조하고 있는 값으로 유지가 되는 것을 확인할 수 있다!
지금까지 useRef
에 대해 알아보았는데 나는 주로 DOM
노드를 조작하기 위해 사용했었는데 참조 값을 유지하는 기능은 이번에 알게되었다.
자주 사용하는 hook은 아니겠지만 그래도 이렇게 한 번 알아두면 추후에 필요할 때 번뜩 생각나서 사용하지 않을까 생각한다.
.
.
.
.
.
.
참고사이트
[daleseo] - useRef 사용법
[React 공식 문서] - Ref로 DOM 조작
[React 공식 문서] - Ref로 값 참조