-
ref & useRef() & forwardRef()리액트 2021. 9. 11. 20:16
공부하기 위해 리액트 공식 문서 및 리액트 블로그를 정리한 내용입니다.
Ref
일반적으로 리액트에서 DOM의 어느 자식을 수정하기 위해서는 새로운 props를 전달하여 자식을 다시 렌더링해야합니다. 그러나 이런 일반적인 방법을 벗어나 직접적으로 자식을 수정해야 하는 경우도 있습니다. 그 중 하나의 방법으로 Ref를 사용하실 수가 있습니다.
Ref를 사용해야할 때
리액트는 다음과 같은 사례에 Ref를 사용할 것을 권장합니다
- focus, text selection, 미디어 재생 을 관리해야할 경우
- 애니메이션을 직접 실행해야하는 경우
- 서드 파티 DOM 라이브러리를 리액트와 같이 사용할 경우
Ref 생성하기
class 컴포넌트
class MyComponent extends React.Component { constructor(props) { super(props); this.myRef = React.createRef(); // 중요 } render() { return <div ref={this.myRef} />; // 중요 } }
Ref는 React.createRef()를 통해 생성하고 ref 속성을 통해 React 엘리먼트에 부착을 합니다.
Ref에 접근하기
const node = this.myRef.current;
위 코드의 render 메서드 중에 ref={this.myRef} 부분을 보겠습니다. 해당 코드를 통해 노드를 향한 참조는 ref.current 어트리뷰트에 담기게 됩니다. 즉 this.myRef.current의 값은 ref={this.myRef} 코드가 적힌 엘리먼트입니다.
<div ref={this.myRef} /> 엘리먼트를 제어하고 싶다면 this.myRef.current를 통해 제어를 하실 수가 있습니다.
클래스 컴포넌트와 Ref 사용 예
class CustomTextInput extends React.Component { constructor(props) { super(props); // textInput DOM 엘리먼트를 저장하기 위한 ref를 생성합니다. this.textInput = React.createRef(); this.focusTextInput = this.focusTextInput.bind(this); } focusTextInput() { // DOM API를 사용하여 명시적으로 text 타입의 input 엘리먼트를 포커스합니다. // 주의: 우리는 지금 DOM 노드를 얻기 위해 "current" 프로퍼티에 접근하고 있습니다. this.textInput.current.focus(); } render() { // React에게 우리가 text 타입의 input 엘리먼트를 // 우리가 생성자에서 생성한 `textInput` ref와 연결하고 싶다고 이야기합니다. return ( <div> <input type="text" ref={this.textInput} /> <input type="button" value="Focus the text input" onClick={this.focusTextInput} /> </div> ); } }
위 코드는 input[type=button]을 클릭하면 input[type=text]에 focus가 되도록 하고자 합니다. input[type=button]을 클릭하면 focusTextInput 함수를 실행시킵니다. focusTextInput 함수는 ref를 통해 참조한 input[type=text]를 this.textInput.current를 통해 참조한다는 것을 아실 수가 있습니다.
좀 더 구체적으로 말하자면 컴포넌트가 마운트될 때 React가 this.textInput.current 에 DOM 엘리먼트를 대입하는 것입니다. 그리고 컴포넌트의 마운트가 해제될 때 this.textInput.current의 값을 null로 돌려 놓습니다.
함수 컴포넌트
함수 컴포넌트에서 ref를 사용하기 위해서는 useRef() 훅을 사용해야합니다.
const refContainer = useRef(initialValue);
useRef는 .current 프로퍼티로 전달된 인자(initialValue)로 초기화된 변경 가능한 ref 객체를 반환합니다.
즉 refContainer.current의 값은 initialValue인 상태로 초기화 됩니다.
함수 컴포넌트와 useRef() 사용 예
function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { // `current` points to the mounted text input element inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); }
ref={inputEl}을 통해 리액트는 변경될때마다 변경된 DOM노드를 .current 프로퍼티를 설정합니다.
useRef() 특징
useRef()를 DOM의 특정 노드를 참조하는 것 외에도 다양하게 사용할 수 있습니다. useRef()를 통해 {current : ...} 객체를 생성하는데, 매번 렌더링할 때마다 동일한 ref 객체를 제공하기 때문에 어떤 가변값을 유지하는 데에도 사용할 수가 있습니다.
가변값 유지를 위한 useRef() 예시
import React, { useEffect, useState } from "react"; const App = () => { const [name, setName] = useState(""); const [renderCount, setRenderCount] = useState(0); useEffect(() => { setRenderCount((prevRenderCount) => prevRenderCount + 1); }); return ( <> <input value={name} onChange={(e) => setName(e.target.value)} /> <div>My name is {name}</div> <div>I rendered {renderCount} times</div> </> ); }; export default App;
input에 작성할 때마다 렌더링을 몇번하는지 알려주도록 작성하고자 합니다. 위 코드를 보면 될듯 싶지만 무한으로 렌더링을 합니다.
위 코드를 실행하면 아래와 같이 나옵니다.
위 문제를 해결하기 위해 ueRef()를 사용할 수도 있습니다.
해결법 #1
useEffect(() => { setRenderCount((prevRenderCount) => prevRenderCount + 1); }, [name]);
본래 이렇게 해결할 수 있습니다만 useRef()를 공부하고자 사용해보겠습니다.
해결법 #2
import React, { useEffect, useState, useRef } from "react"; const App = () => { const [name, setName] = useState(""); const renderCount = useRef(1); useEffect(() => { renderCount.current = renderCount.current + 1; }); return ( <> <input value={name} onChange={(e) => setName(e.target.value)} /> <div>My name is {name}</div> <div>I rendered {renderCount.current} times</div> </> ); }; export default App;
결과는 아래와 같습니다
원하는대로 정상적으로 작동합니다!
ForwardRef
리액트를 사용하다보면 button, input 등의 컴포넌트를 재사용하기 위해 코드를 작성하는 경우가 많습니다. 아래는 어느 버튼 컴포넌트입니다.
function FancyButton(props) { return ( <button className="FancyButton"> {props.children} </button> ); }
어떻게 작성을 해야 해당 컴포넌트가 ref를 child한테 전달을 할 수 있도록 할까요?
React.forwardRef를 사용하면 해결이 됩니다.
const FancyButton = React.forwardRef((props, ref) => ( <button ref={ref} className="FancyButton"> {props.children} </button> )); // 이제 DOM 버튼으로 ref를 작접 받을 수 있습니다. const ref = React.createRef(); <FancyButton ref={ref}>Click me!</FancyButton>;
forwardRef을 통해 일부 컴포넌트가 수신한 ref를 조금 더 아래로 전달할 수가 있습니다.
주의사항
- React.forwardRef를 사용 시 사용하기 전에 비해 리액트가 동작하는 방법이 크게 바뀔 가능성이 높아지며 이전 동작에 의존하는 앱이나 다른 라이브러리들이 손상될 가능성도 높아집니다.
- 이 때문에 조건적으로 React.forwardRef가 존재할 때 조건부로 적용하는 것을 권장하지 않습니다.
참고자료
'리액트' 카테고리의 다른 글
클래스101 클론 후기 (0) 2022.01.16 react-router-dom (0) 2021.09.15 Portals (0) 2021.09.08 Context (0) 2021.09.05 Lists & Keys & Reconciliation (0) 2021.08.29