컴포넌트가 특정 정보를 '기억'하도록 하고 싶지만, 해당 정보가 새 렌더링을 촉발하지 않도록 하려는 경우 ref를 사용할 수 있다.
Adding a ref to your component(컴포넌트에 ref 추가하기)
// 컴포넌트 내부에서 useRef 훅을 호출하고 참조할 초기값을 인자로 전달
const ref = useRef(0);
// useRef는 다음과 같은 객체를 반환
{
current: 0 // The value you passed to useRef
}
- ref.current 속성을 통해 해당 ref의 현재 값에 엑세스할 수 있다.
- 해당 값은 의도적으로 변이가 가능하므로 읽기와 쓰기가 모두 가능하다.
- ref는 state와 마찬가지로 문자열, 객체, 함수 등 무엇이든 가리킬 수 있다.
- state와 달리 ref는 current 속성을 읽고 수정할 수 있는 일반 JavaScript 객체이다.
- ref가 변경될 때마다 리렌더링되지 않는다.
Example: building a stopwatch(예제: 스톱워치 만들기)
- ref와 state를 단일 컴포넌트로 결합할 수 있다.
- 아래 예제에서는 "Stop" 버튼을 눌렀을 때, clearInterval을 호출해야하고, 그때 필요한 intervalId는 렌더링에 사용되지 않으므로 ref에 보관할 수 있다.
import { useState, useRef } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
const intervalRef = useRef(null);
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
function handleStop() {
clearInterval(intervalRef.current);
}
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
console.log(1)
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
Start
</button>
<button onClick={handleStop}>
Stop
</button>
</>
);
}
Differences between refs and state(ref와 state의 차이점)
refs | state |
useRef(initiallValue)는 { current : initialValue }을 반환 | useState(initialValue)는 state 변수의 현재값과 state 설정자 함수([value, setValue])를 반환 |
변경 시 리렌더링을 촉발하지 않음 | 변경 시 렌더링을 촉발함 |
Mutable - 렌더링 프로세스 외부에서 current 값을 수정하고 업데이트할 수 있음 | "Immutable" - state setting 함수를 사용하여 state 변수를 수정해 리렌더링을 대기열에 추가해야함 |
렌더링 중에는 current 값을 읽거나 쓰지 않아야 함 | 언제든지 state를 읽을 수 있음. 각 렌더링에는 변경되지 않는 자체 state snapshot이 있음 |
* 렌더링 중에는 current 값을 읽거나 쓰지 않아야 함.
1. 읽기 : 렌더링 중에 useRef 객체의 current 값을 읽는 행위는 React의 라이프사이클과 관련된 문제를 발생시킬 수 있다. 이로 인해 예기치않은 부작용이 발생할 수 있으며, 컴포넌트의 불필요한 리렌더링을 유발할 수 있다.
// 잘못된 예시
import React, { useRef } from 'react';
function Counter() {
const countRef = useRef(0);
// 잘못된 사용: 렌더링 중에 useRef 객체의 current 값을 읽음
console.log('Current count:', countRef.current);
return (
<div>
{/* ... */}
</div>
);
}
// 올바른 예시
import React, { useRef } from 'react';
function Counter() {
const countRef = useRef(0);
// 올바른 사용: useEffect 내부에서 useRef 객체의 current 값을 읽음
React.useEffect(() => {
console.log('Current count:', countRef.current);
}, []);
return (
<div>
{/* ... */}
</div>
);
}
2. 쓰기 : 렌더링 중에 useRef 객체의 current 값을 변경하는 행위 역시 문제를 일으킬 수 있다. 이는 React의 가상 DOM 및 리렌더링 메커니즘과 충돌할 수 있으며, 예상치 못한 결과를 초래할 수 있다.
// 잘못된 예시
import React, { useRef } from 'react';
function Counter() {
const countRef = useRef(0);
// 잘못된 사용: 렌더링 중에 useRef 객체의 current 값을 변경
countRef.current += 1;
return (
<div>
{/* ... */}
</div>
);
}
// 올바른 예시
import React, { useRef } from 'react';
function Counter() {
const countRef = useRef(0);
// 올바른 사용: 이벤트 핸들러 내부에서 useRef 객체의 current 값을 변경
const handleIncrement = () => {
countRef.current += 1;
console.log('Updated count:', countRef.current);
};
return (
<div>
<button onClick={handleIncrement}>Increment</button>
</div>
);
}
- 즉, useRef의 current 값을 읽거나 변경하려면, 주로 React의 라이프사이클 메서드나 useEffect 훅과 같은 리렌더링과 관련된 사이클 외부에서 수행하는 것이 좋다. 일반적으로 컴포넌트가 마운트되거나 업데이트되는 시점 이후에 useEffect 내부에서 useRef의 current 값을 조작하는 것이 안전하다.
* ref는 내부에서 어떻게 작동하나요?
- useRef는 useState 위에 구현될 수 있고, React 내부에서 useRef는 다음과 같이 구현된다고 상상할 수 있다.
// Inside of React
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}
- 첫 번째 렌더링 중에 useRef는 { current: initialValue } 를 반환한다. useRef는 항상 동일한 객체를 반환해야 하기 때문에, state setter는 불필요하다.
- ref를 설정자가 없는 일반 state 변수라고 생각하면 된다.
When to use refs(ref를 사용해야 하는 경우)
- 일반적으로 ref는 컴포넌트가 React로부터 컴포넌트의 형상에 영향을 주지 않는 브라우저 API 등과 통신해야 할 때 사용한다.
1. timeout ID 저장
import React, { useRef } from 'react';
function Timer() {
const timeoutId = useRef(null);
const startTimer = () => {
clearTimeout(timeoutId.current);
timeoutId.current = setTimeout(() => {
console.log('Timeout has occurred');
}, 2000);
};
return (
<div>
<button onClick={startTimer}>Start Timer</button>
</div>
);
}
2. 다음 페이지에서 다룰 DOM elements 저장 및 조작
import React, { useRef } from 'react';
function ElementManipulation() {
const divRef = useRef(null);
const handleColorChange = () => {
divRef.current.style.backgroundColor = 'blue';
};
return (
<div>
<div ref={divRef} style={{ width: '100px', height: '100px', backgroundColor: 'red' }}>
This is a div
</div>
<button onClick={handleColorChange}>Change Color</button>
</div>
);
}
3. JSX를 계산하는 데 필요하지 않은 다른 객체 저장
import React, { useRef, useState } from 'react';
function ExpensiveCalculation() {
const resultRef = useRef(null);
const [showResult, setShowResult] = useState(false);
const calculate = () => {
// Some expensive calculation
const result = Math.random() * 100;
resultRef.current = result;
setShowResult(true);
};
return (
<div>
<button onClick={calculate}>Calculate</button>
{showResult && <p>Result: {resultRef.current}</p>}
</div>
);
}
- 즉, 컴포넌트에 일부 값을 저장해야 하지만 렌더링 로직에는 영향을 미치지 않는 경우 ref를 선택.
Best practices for refs(ref 모범 사례)
- 컴포넌트의 예측 가능성 높일 수 있는 원칙
1. ref를 탈출구로 취급하기 : ref는 외부 시스템이나 브라우저 API로 작업할 때 유용하지만, 애플리케이션 로직과 데이터 흐름의 대부분이 ref에 의존하는 경우 접근 방식을 재고해봐야 할 수도 있다.
2. 렌더링 중에는 ref.current를 읽거나 쓰지 않기 : 렌더링 중에 일부 정보가 필요한 경우, 대신 state를 사용하는 것이 좋다. React는 ref.current가 언제 변경되는지 알지 못하기 때문에, 렌더링 중에 읽어도 컴포넌트의 동작을 예측하기 어렵기 때문이다.
(유일한 예외는 첫 번째 렌더링 중에 ref를 한 번만 설정하는 if(!ref.current) ref.current = newThing()과 같은 코드이다.)
- ref는 state와 달리 현재 값을 변이하면 즉시 변경된다.(state는 모든 렌더링에 대해 스냅샷처럼 작동하며 동기적으로 업데이트되지 않는다.)
- 위와 같은 특징은 ref 자체가 일반 JavaScript 객체이기 때문이다.
- ref로 작업할 때 변이하는 객체가 렌더링에 사용되지 않는 한, ref로 변이를 일으켜도 된다.
Refs and the DOM(Ref와 DOM)
- ref는 모든 값을 가리킬 수 있지만, 가장 일반적인 사용 사례는 DOM 요소에 엑세스하는 것이다.
ex. input에 focus를 맞추고자 할때, <div ref={myRef}>와 같이 JSX의 ref 어트리뷰트에 ref를 전달하면 React는 해당 DOM 엘리먼트를 myRef.current에 넣는다.
* 참고 : React 공식문서(https://react-ko.dev/learn)