728x90
의의와 배경
useRef
는 리렌더링 간에도 유지되는 가변 저장소를 제공한다. 값 변경이 렌더를 유발하지 않는다는 점에서 useState
와 구분되며, 컴포넌트 인스턴스마다 독립적으로 유지된다는 점에서 단순 let
과도 다르다. 주로 DOM 접근, 타이머/외부 핸들 저장, 최신 값 보관 등 렌더와 무관한 정보를 관리할 때 사용한다.
목차
- 개념 정리: 렌더링 모델과
ref.current
- 비교:
useState
vsuseRef
vslet
- 사용 시나리오와 코드 예시
- 주의사항(안티패턴)
- 체크리스트
- 결론
개념 정리
useRef<T>(initial)
는{ current: T }
형태의 객체를 반환한다.ref.current
를 변경해도 컴포넌트는 리렌더링되지 않는다.- 같은 컴포넌트 인스턴스에서 렌더 간 동일한
ref
객체가 유지된다. - 의도: “렌더 출력에 직접 참여하지 않는, 그러나 렌더 사이에 유지돼야 하는 값”을 담는다.
비교: 언제 무엇을 쓰나
항목 | useState | useRef | let(컴포넌트 내부) |
---|---|---|---|
렌더 트리거 | 값 변경 시 렌더 | 렌더 없음 | 렌더마다 초기화됨 |
값 유지(렌더 간) | O | O | X |
인스턴스별 독립성 | O | O | O(내부이지만 매 렌더 초기화) |
주 용도 | UI에 반영돼야 하는 상태 | 렌더와 무관한 가변 값, DOM 핸들 | 순수 계산용 임시 변수 |
주의: 컴포넌트 “외부 모듈 스코프”에 let
을 두면 모든 인스턴스가 공유하므로 권장하지 않는다.
사용 시나리오와 코드 예시
1) DOM 접근과 제어
import { useRef } from 'react';
export default function FocusInput() {
const inputRef = useRef<HTMLInputElement | null>(null);
return (
<div>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>포커스</button>
</div>
);
}
2) 타이머/외부 핸들 저장(클린업 용이)
import { useEffect, useRef } from 'react';
export default function Ticker() {
const intervalId = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
intervalId.current = setInterval(() => {
console.log('tick');
}, 1000);
return () => {
if (intervalId.current) clearInterval(intervalId.current);
};
}, []);
return <div>running...</div>;
}
3) 렌더를 유발하지 않는 카운터/측정값
import { useRef } from 'react';
export default function ClickCounter() {
const clicks = useRef(0);
return (
<button onClick={() => { clicks.current += 1; console.log(clicks.current); }}>
클릭(화면 숫자는 안 바뀜)
</button>
);
}
4) 최신 값 보관으로 stale closure 방지
import { useEffect, useRef } from 'react';
export function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;
return ref; // 항상 최신 값을 가리킴
}
export default function Worker({ onMessage }: { onMessage: (s: string) => void }) {
const latestHandler = useLatest(onMessage);
useEffect(() => {
const id = setInterval(() => {
latestHandler.current('ping'); // 최신 핸들러 호출
}, 1000);
return () => clearInterval(id);
}, []);
return null;
}
주의사항(안티패턴)
ref.current
로 뷰를 업데이트하려 하지 말 것. 뷰에 반영돼야 하는 값은useState
로 관리한다.- 렌더 중
ref.current
를 변경해 렌더 결과에 의존하는 로직을 만들지 말 것. 사이드이펙트는 이펙트 훅에서 처리한다. - 모듈 스코프의
let
으로 컴포넌트 간 상태를 공유하지 말 것(인스턴스 간 간섭 발생). ref
남용으로 상태 추적이 어려워지지 않도록, “UI에 영향”이면state
, “렌더 무관”이면ref
를 기본 규칙으로 삼는다.
체크리스트
- 이 값이 화면에 반영돼야 하는가? 그렇다면
useState
를 사용한다. - 렌더 사이에 유지돼야 하지만 화면에 직접 반영되지 않는가?
useRef
를 사용한다. - 외부 자원 핸들(타이머/소켓/DOM)을 저장하는가?
useRef
로 보관하고 이펙트에서 관리한다. - 모듈 전역
let
로 상태를 공유하고 있지 않은가? 인스턴스 격리를 보장하도록 수정한다.
결론
useRef
는 “렌더와 분리된 가변 값”을 안전하게 보존하는 도구다. DOM 접근, 타이머·외부 핸들 저장, 최신 콜백 유지 등에서 특히 유용하다. 반대로 뷰에 영향을 주는 값은 useState
로 관리해 예측 가능한 렌더 사이클을 유지하는 것이 바람직하다.
728x90
'Frontend Development' 카테고리의 다른 글
Next.js 최신 캐싱 전략 총정리 (0) | 2025.09.03 |
---|---|
면접에서 묻는 "의존성 주입 경험이 있나요?"의 의미 (1) | 2025.08.26 |
Core Web Vitals: LCP, INP, CLS 개념과 개선 방법 (3) | 2025.08.20 |
코드가 깔끔해지는 비밀: 프론트엔드에서 함수형 프로그래밍 활용하기 (3) | 2025.08.18 |
프론트엔드 개발자를 위한 AI 용어 완전 정리 (3) | 2025.07.29 |