[React18] 10. ref
컴포넌트가 일부 정보를 기억하고 싶지만 해당 정보가 렌더링을 유발하지 않도록 하려면 ref를 사용할 수 있습니다.
React에서 userRef 훅을 가져와 컴포넌트에 ref를 추가할 수 있습니다. 참조할 초기값을 유일한 인자로 전달하며 useRef는 {current: 초기값}과 같은 객체를 반환합니다. state와 마찬가지로 문자열, 객체, 함수 등 무엇이든 가리킬 수 있습니다. ref.current 속성을 통해 ref의 현재 값을 접근할 수 있습니다.
렌더링에 정보가 사용되는 경우 state로 유지하고, 이벤트 핸들러만 정보를 필요로 하고 변경해도 다시 렌더링 할 필요가 없는 경우 ref를 사용하는 것이 더 효율적입니다. 또한 useState로 인해 리렌더링 되더라도 기존에 있는 값을 유지합니다.
일반적으로 컴포넌트가 timeout id 저장할 경우, 외부 시스템 또는 DOM 엘리먼트 저장 및 조작할 경우, JSX를 계산하는 데 필요하지 않은 정보를 저장할 경우에 사용하며 렌더링 로직에 영향을 미치지 않는 경우에 ref를 선택합니다.
** ref 자체가 일반 JavaScript 객체이므로 current 값을 변조하면 즉시 변경됩니다. (mutation 하더라도 React는 ref 훅이 어떻게 처리하든 신경 쓰지 않습니다.)
import {useRef} from 'react';
export default function Login(){
const ref=useRef('');
function handleInput(event){
ref.current=event.target.value;
}
return (
<input onInput={handleInput}/>
);
}
ref로 DOM 조작하기
React는 렌더링 결과물에 맞춰 DOM 변경을 자동으로 처리하기 때문에 컴포넌트에서 자주 DOM을 조작해야 할 필요는 없습니다. 하지만 특정 노드에 포커스를 옮기거나, 스크롤 위치를 옮기거나, 위치와 크기를 측정하기 위해 React가 관리하는 DOM 요소에 접근해야 할 때가 있습니다. 이때 ref를 DOM 노드에 접근하기 위해 ref가 필요합니다.
JSX의 ref 속성에 ref를 전달하면 React는 DOM 엘리먼트를 ref.current에 넣습니다. DOM에서 사라지면 React는 ref.current 값을 null로 업데이트합니다. ref를 이용해 이벤트 핸들러에서 접근하거나 노드에 정의된 내장 브라우저 API를 사용할 수 있습니다. (ex ref.current.scrollIntoView())
import {useRef} from 'react';
export default function Login(){
const inputRef=useRef(null);
function handleClick(){
inputRef.current.focus();
}
return (
<>
<input ref={inputRef}/>
<button onClick={handleClick}>Click me</button>
</>
);
}
사용자 정의 컴포넌트에 ref를 주입할 때는 null이 기본적으로 주어집니다. React는 기본적으로 다른 컴포넌트의 DOM에 접근하는 것을 허용하지 않습니다. 대신 특정 컴포넌트에서 소유한 DOM 선택적으로 노출할 수 있습니다. 컴포넌트 자식 중 하나에 ref를 전달하도록 지정할 수 있습니다.
React에서는 ref은 props로 인식하지 않습니다. ref 네이밍을 변경하거나 DOM 노드를 노출하기 원하는 컴포넌트에 forwradRef API를 사용하면 됩니다.
** 디자인 시스템에서 버튼, 입력 등과 같은 저수준 컴포넌트는 해당 ref를 DOM 노드로 전달하는 것이 일반적인 패턴입니다. 하지만 목록이나 입력 폼과 같은 상위 수준 컴포넌트는 반적으로 DOM 구조에 대한 우발적 의존성을 피하기 위해 해당 DOM 노드를 노출하지 않습니다.
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
import {useRef} from 'react';
import {MyInput} from './MyInput.js';
export default function Login(){
const inputRef=useRef(null);
function handleClick(){
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef}/>
<button onClick={handleClick}>Click me</button>
</>
);
}
** 부모 컴포넌트에서 focus 외에도 DOM을 직접 변경하는 등 예상치 못한 작업을 할 수 있습니다. 몇몇 상황에서는 노출된 기능을 제한하고 싶을 때 useImperativeHandle을 사용합니다. 여기 컴포넌트 내부 ref는 실제 DOM을 가지고 있습니다. 하지만 useImperativeHandle을 사용하여 React가 ref를 참조하는 부모 컴포넌트에는 useImperativeHandle 호출에서 직접 구성한 객체를 전달하도록 지시합니다.
const MyInput = forwardRef((props, ref) => {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// Only expose focus and nothing else
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} ref={realInputRef} />;
});
React가 ref를 부여할 때
일반적으로 렌더링 하는 중에 ref에 접근하는 는 것은 좋지 않습니다. 첫 렌더링에서 DOM 노드가 아직 생성되지 않았으므로 ref.current는 null인 상태입니다. 갱신에 의한 렌더링은 아직 DOM 노드가 업데이트되지 않는 상태입니다. 두 상황 모두 ref를 읽기 너무 이른 상황입니다.
React는 커밋 단계에 ref.current를 설정합니다. React는 DOM이 업데이트되기 전에는 ref.current의 값을 null로 설정하였다가, DOM이 업데이트된 직후 해당 DOM 노드로 다시 설정합니다.
만약 state 업데이트한 후 scroll을 변경하고 싶다면 React가 DOM을 동기적으로 업데이트(“플러시”)하도록 강제할 수 있습니다. state 업데이트를 flushSync 호출로 감싸면 코드가 실행된 직후 React가 동기적으로 DOM을 변경하도록 지시합니다. (useEffect을 활용하는 방법도 있지만 렌더링 후에 일어나기 때문에 늦다는 단점이 있습니다.)
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
ref는 React에서 벗어나야 할 때만 사용해야 합니다. 포커스, 스크롤 관리 같이 비파괴적인 행동을 고수한다면 문제를 마주치지 않을 것입니다. 만약 DOM을 직접 수정하는 시도를 한다면 React가 만들어내는 변경 사항과 충돌을 발생시킬 위험을 감수해야 합니다. (DOM을 직접 변경했을 때 React는 DOM을 올바르게 관리하는 방법을 모릅니다.) 그러니 React가 관리하는 DOM을 직접 바꾸려 하지 않는 것이 좋습니다.
** React18 공식 문서 보고 정리한 내용입니다.
https://ko-react-exy5xcwjj-fbopensource.vercel.app/learn/referencing-values-with-refs
Ref로 값 참조하기 – React
The library for web and native user interfaces
ko.react.dev
https://ko-react-exy5xcwjj-fbopensource.vercel.app/learn/manipulating-the-dom-with-refs
Ref로 DOM 조작하기 – React
The library for web and native user interfaces
ko.react.dev