Overview.
- 렌더링에 필요하지 않은 값 참조
- 따라서 값이 변경되도 컴포넌트 리렌더링은 발생하지 않음
- DOM 요소 참조하거나 렌더링 영향이 없는 변수를 선언할 때 씀
- 스타일 변경하기, 포커싱 하기 등의 DOM 컨트롤
Shape.
import { useRef } from 'react';
function MyComponent() {
const a = useRef(0);
const b = useRef(null);
}
매개변수
useRef
는 매개변수로initialValue
를 받을 수 있음- 이는
current
프로퍼티 초기 설정값임 - 어떤 유형의 값도 올 수 있음
- 초기 렌더링 이후부터 무시됨
반환값
- 단일 프로퍼티를 가진
ref
객체를 반환함 initialValue
로 설정된current
를 바꾸거나,ref
객체를current
프로퍼티로 설정함
주의
ref.current
프로퍼티는 state와 달리 변이할 수 있음 -> mutable- 그러나 렌더링에 사용되는 객체를 포함하는 경우 해당 객체를 변이해선 안됨
ref.current
프로퍼티를 변경해도 컴포넌트는 리렌더링하지 않음ref
는 일반 자바스크립트 객체 -> React는 사용자가 언제 변경했는지 모름- 초기화를 제외하고 렌더링 중에
ref.current
를 쓰거나 읽어선 안됨
주의에 대한 자세한 설명
- React 렌더링 원칙과 관련 있음
- 예측 가능성과 일관성
- React 렌더링은 순수해야 함
- 즉, 같은
props
와state
에 대해 항상 같은 JSX를 반환해야 함 - 렌더링 중에 참조하는 객체를 직접 변이하면 이 순수성이 깨짐
의도하지 않은 부작용
function BadComponent() { const objRef = useRef({ count: 0 }) if(objRef.current.count < 5) { objRef.current.count += 1 // 렌더링 중 변이 } return <>{objRef.current.count}</> }
- 위 경우에 렌더링할 때 마다
count
증가 - 때문에 개발 모드에서 두 번 렌더링 됨 -> 예상치 못한 결과
- 다른 컴포넌트가 같은 객체를 참조하면다면 -> 예측 불가능성
function GoodComponent() {
const objRef = useRef({ count: 0 })
const handleClick = () => {
// 이벤트 핸들러에서 변이하도록 함
objRef.current.count += 1;
// 필요에 따라 setState로 리렌더링
}
return <button onClick={handleClick}>{objRef.current.count}</button>
}
ref
는 DOM 노드나 타이머 ID와 같은 렌더링과 무관한 값을 저장해야 함- 렌더링에 영향을 주는 데이터는
state
로 관리해야 함
Usage.
import { useState, useRef } from 'react';
export default function Stopwatch() {
// 스톱워치 시작 시간과 현재 시간 상태 관리
const [startTime, setStartTime] = useState(null);
// 현재 시간을 계속 업데이트하며 저장
const [now, setNow] = useState(null);
// setInterval ID를 저장하기 위한 ref
// 렌더링과 무관하고, 리렌더링되어도 유지됨
const intervalRef = useRef(null);
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setNow(Date.now()); // 10ms마다 현재 시간 업데이트
}, 10);
}
function handleStop() {
clearInterval(intervalRef.current);
}
// 경과 시간 계산 로직
let secondsPassed = 0;
if (startTime != null && now != null) {
// 현재 시간에서 시작 시간을 빼면 경과 시간임
// 밀리초를 초 단위로 나눔
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
Start
</button>
<button onClick={handleStop}>
Stop
</button>
</>
);
}
ref로 DOM 조작하기
- 우선
initialValue
에null
인ref
객체 선언
function MyComponent() {
const inputRef = useRef(null)
}
- 그 다음
ref
객체를ref
어트리뷰트로 조작하려는 DOM 노드의 JSX에 전달
return <input ref={inputRef} />
- 리액트가 DOM 노드를 생성하고 화면에 그리면,
ref
객체의current
프로퍼티를 DOM 노드로 설정함 - DOM 노드는
<input>
에 접근해focus()
같은 메서드를 호출할 수 있게됨
function handleClick() {
inputRef.current.focus()
}
- 노드가 화면에서 제거되면
current
는null
이 됨
ref 콘텐츠 재생성 피하기
function Video() {
const playerRef = useRef(new VidoePlayer())
}
- 위와 같은 코드는 컴포넌트가 리렌더링될 때 마다 비디오 플레이어 객체를 새로 생성해
ref
객체에 할당함 -> 낭비 발생
function Video() {
const playerRef = useRef(null);
if(playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
}
- 이 경우에는 결과가 항상 동일하고 초기화 중에만 조건이 실행됨 -> 충분히 예측 가능
커스텀 컴포넌트에 대한 ref 얻기
const inputRef = useRef(null)
return <MyInput ref={inputRef} />
- 컴포넌트에
ref
를 전달하려고 하면 에러 발생
⛔️ 경고: 함수 컴포넌트에는 ref를 지정할 수 없습니다. 이 ref에 접근하려는 시도는 실패합니다. React.forwardRef()를 사용하려고 하셨나요?
- 기본적으로 컴포넌트는 내부 DOM 노드에 대한
ref
를 외부로 노출하지 않음 - 이 문제를 해결하려면
forwardRef
로ref
를 가져오고자 하는 컴포넌트를 감싸야 함 - 그러면 부모 컴포넌트에서
ref
를 가져올 수 있음
const MyInput = forwardRef(({ value, onChange }, ref) => {
return (
<input
value={value}
onChange={onChange}
ref={ref}
/>
)
})
export default MyInput
- 아래는 이해를 돕는 예제
import { useRef, forwardRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
})
export default function MyForm() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
Results.
- 렌더링에 필요하지 않은 값은
useRef
로 관리할 수 있음 - 렌더링 중에
ref.current
프로퍼티를 읽거나 쓰면 안됨- 렌더링에 영향을 주는 데이터는
state
로 관리할 것
- 렌더링에 영향을 주는 데이터는
- 주로 DOM을 조작하는데 쓰임
- 부모 컴포넌트가 자식 컴포넌트를
ref
로 사용하려면 자식 컴포넌트는forwardRef
로 감싸야 함