React의 useRef 훅을 활용하여 DOM 요소에 직접 접근하고 렌더링과 무관하게 값을 유지하는 방법을 알아봅니다. 실무에서 자주 사용되는 예제와 함께 useRef의 모든 것을 정리했습니다.
웹 앱에서 특정 요소에 포커스를 주거나 스크롤 위치를 조정할 때마다 복잡한 코드를 작성해야 해서 불편했던 적이 있으신가요?
React는 가상 DOM을 통해 UI를 효율적으로 관리하지만, 때로는 실제 DOM 요소에 직접 접근하거나 컴포넌트가 리렌더링되어도 값을 그대로 유지해야 할 때가 있습니다. 이런 상황에서 useRef 훅은 아주 유용한 해결책이 됩니다.
useRef의 기본 개념
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
importReact,{useRef}from'react';function입력폼(){// useRef 생성
const입력칸Ref=useRef(null);const포커스하기=()=>{// useRef로 DOM 요소에 직접 접근
입력칸Ref.current.focus();};return(<div><inputref={입력칸Ref}type="text"/><buttononClick={포커스하기}>입력칸포커스</button></div>);}
useRef는 무엇인가요?
useRef는 DOM 요소를 직접 제어할 수 있게 해주는 도구입니다. 일상생활에 비유하자면, TV를 보면서 채널을 바꾸고 싶을 때 리모컨을 사용하는 것과 같습니다. React에서는 이 ‘리모컨’(useRef)을 사용해 원하는 요소를 찾아 제어할 수 있습니다.
어떻게 사용하나요?
먼저 useRef()로 리모컨을 만듭니다. (예: const 입력칸Ref = useRef(null))
DOM 요소에 이 리모컨을 연결합니다. (예: <input ref={입력칸Ref} />)
이제 입력칸Ref.current로 언제든지 그 요소를 제어할 수 있습니다.
실제 사용 예시
위 예제에서는 버튼을 클릭하면 입력칸에 자동으로 커서가 위치하도록 만들었습니다. 마치 리모컨의 버튼을 눌러 TV의 특정 기능을 실행하는 것처럼, 입력칸Ref.current.focus()는 입력칸에 커서를 위치시키라는 명령을 내리는 것입니다.
importReact,{useRef,useState,useEffect}from'react';function렌더링카운터(){// 일반 변수 - 리렌더링될 때마다 초기화됨
let일반변수=0;// useState - 값 변경 시 리렌더링 발생
const[상태값,상태변경]=useState(0);// useRef - 리렌더링 시에도 값 유지, 값 변경해도 리렌더링 하지 않음
const렌더링횟수=useRef(0);useEffect(()=>{일반변수++;렌더링횟수.current++;console.log(`일반변수: ${일반변수}, 렌더링횟수: ${렌더링횟수.current}`);});return(<div><p>렌더링횟수:{렌더링횟수.current}</p><p>상태값:{상태값}</p><buttononClick={()=>상태변경(상태값+1)}>상태증가</button></div>);}
값 유지하기 일반 변수와 달리, useRef로 생성한 객체의 .current 값은 컴포넌트가 리렌더링되어도 초기화되지 않습니다.
위 예제에서 버튼을 클릭하면 컴포넌트가 리렌더링되고, 일반변수는 다시 0이 되지만 렌더링횟수.current는 계속 증가합니다.
화면 업데이트 없음 useState와 달리 useRef의 .current 값이 변경되어도 컴포넌트가 리렌더링되지 않습니다.
이는 성능 최적화에 도움이 되지만, 값이 바뀌어도 화면에 바로 반영되지 않는다는 점을 주의해야 합니다.
가상 DOM과의 관계 useRef를 통한 DOM 제어는 React의 가상 DOM 시스템을 우회합니다.
이것이 바로 useRef 값이 변경되어도 리렌더링이 발생하지 않는 이유입니다.
functionProductList(){constlastItemRef=useRef(null);const[products,setProducts]=useState([]);const[page,setPage]=useState(1);useEffect(()=>{// IntersectionObserver로 마지막 아이템 감지
constobserver=newIntersectionObserver((entries)=>{if(entries[0].isIntersecting){// 마지막 아이템이 화면에 보이면 다음 페이지 로드
setPage(prevPage=>prevPage+1);}});if(lastItemRef.current){observer.observe(lastItemRef.current);}return()=>observer.disconnect();},[products]);return(<div>{products.map((product,index)=>(<divkey={product.id}>{/* 마지막 아이템에 ref 연결 */}<divref={index===products.length-1?lastItemRef:null}>{product.name}</div></div>))}</div>);}
무한 스크롤 효과적으로 구현하기 useRef와 IntersectionObserver를 함께 사용하면 무한 스크롤을 쉽게 구현할 수 있습니다.
마지막 상품이 화면에 보이면 자동으로 다음 페이지를 불러옵니다.
functionVideoPlayer(){constvideoRef=useRef(null);const[isPlaying,setIsPlaying]=useState(false);consttogglePlay=()=>{if(videoRef.current){// 비디오 요소의 내장 메서드로 재생/정지 제어
if(isPlaying){videoRef.current.pause();}else{videoRef.current.play();}setIsPlaying(!isPlaying);}};return(<divclassName="video-container"><videoref={videoRef}src="/video-path.mp4"onEnded={()=>setIsPlaying(false)}/><buttononClick={togglePlay}>{isPlaying?'일시정지':'재생'}</button></div>);}
미디어 요소 직접 제어하기 비디오나 오디오 같은 미디어 요소를 useRef로 참조하면 play(), pause() 같은 내장 메서드를 직접 호출할 수 있습니다.
functionStateComparisonComponent(){// 화면에 보이는 값 - useState 사용
const[inputValue,setInputValue]=useState("");const[isSubmitted,setIsSubmitted]=useState(false);// 화면에 보이지 않는 내부 값 - useRef 사용
constsubmitCountRef=useRef(0);consttimerRef=useRef(null);consthandleSubmit=(e)=>{e.preventDefault();setIsSubmitted(true);submitCountRef.current+=1;// 3초 후 알림 숨기기
clearTimeout(timerRef.current);timerRef.current=setTimeout(()=>{setIsSubmitted(false);},3000);};return(<formonSubmit={handleSubmit}><inputvalue={inputValue}onChange={(e)=>setInputValue(e.target.value)}/><buttontype="submit">제출</button>{isSubmitted&&<p>제출되었습니다!</p>}</form>);}
선택 기준 가장 중요한 질문은 “이 값이 바뀔 때 화면을 다시 그려야 하나요?“입니다.
화면에 직접 표시되는 값(inputValue, isSubmitted)은 useState를 씁니다.
내부 로직에만 필요한 값(submitCount, timerID)은 useRef를 쓰는 것이 효율적입니다.
렌더링 최적화 불필요한 리렌더링을 줄이려면 화면에 바로 반영할 필요가 없는 값은 useRef로 관리하는 것이 좋습니다.
특히 타이머ID나 이전 상태값 저장 같은 경우에는 useRef가 더 적합합니다.
functionCautionComponent(){constrefValue=useRef(0);const[stateValue,setStateValue]=useState(0);// 잘못된 사용: 렌더링 중에 .current 값 변경
refValue.current+=1;// 이렇게 하면 안 됩니다!
consthandleIncrement=()=>{// 올바른 사용: 이벤트 핸들러 안에서 .current 값 변경
refValue.current+=1;console.log(refValue.current);// 값을 화면에 표시하려면 상태를 업데이트해야 함
setStateValue(refValue.current);};return(<div><p>참조값:{refValue.current}</p><p>상태값:{stateValue}</p><buttononClick={handleIncrement}>증가</button></div>);}
렌더링 중 .current 값 바꾸지 않기 컴포넌트가 렌더링되는 중에 직접 .current 값을 바꾸면 예상치 못한 문제가 생길 수 있습니다.
.current 값은 이벤트 핸들러나 useEffect 안에서만 바꿔야 합니다.
화면 업데이트 주의 useRef 값이 변경되어도 화면이 자동으로 업데이트되지 않습니다.
useRef 값을 화면에 반영하려면 useState와 함께 사용해야 합니다.
DOM 요소 존재 확인하기 DOM 요소에 접근하기 전에 그 요소가 실제로 있는지 확인해야 합니다.
항상 if (refValue.current) { /* 로직 수행 */ } 형태로 안전하게 접근하세요.