요약
Ref를 자식 요소에게 전달하고 싶을 때 사용하는 기법
일반적으로는 부모의 ref를 사용하는 것은 컴포넌트간 의존성을 생성하기 때문에 잘 사용되지 않고, 주로 버튼, 인풋 등과 같은 다른 컴포넌트를 사용하지 않는 말단 요소에서 많이 사용된다.
부모 요소의 값을 자식에서 전달하는 방식으로 흔히 쓰이는 props를 이용해 전달하면 되지 않을까? 생각할 수도 있지만, ref
는 key
와 마찬가지로 react에서 다르게 처리되어 props로 전달되지 않기 때문에 forwardRef
사용이 필수적이다.
그래서 코드를 다음과 같이 ref를 props로 넘겨주는 방식으로 작성하면
function App() {
const textValue = useRef();
console.log(textValue.current);
return (
<div className="App">
<RefInput ref={textValue} />
<div>{textValue.current?.value}</div>
</div>
);
}
export default App;
export const RefInput = ({ ref }) => {
return <input ref={ref} type={"text"} />;
};
다음과 같이 forwardRef
를 쓰라고 경고를 보낸다.
근데 props이름을 ref가 아닌 다른 이름으로 ref를 내려주는 경우엔 에러 없이 잘 내려가긴 한다.. 🤨
// App.js <RefInput passref={textValue} /> // RefInput.jsx export const RefInput = ({ passref }) => { return <input ref={passref} type={"text"} />; };
올바른 방법으로 사용하기 위해선 다음과 같이 forwardRef
를 적용해 내려주어야 한다.
import { forwardRef } from "react";
export const RefInput = forwardRef((props, ref) => {
return <input ref={ref} type={"text"} />;
});
고차 컴포넌트는 모든 props를 래핑된 컴포넌트에 전달하는 것이 원칙이지만, props에 ref
는 전달되지 않기 때문에 원하는 대로 작동하지 않는다.
// 공식문서에 나와있는 예시
function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
const {forwardedRef, ...rest} = this.props;
return <Component ref={forwardedRef} {...rest} />;
}
}
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
Hook의 등장으로 HOC의 필요성이 낮아졌다. 고럼 훅에서는 ref 전달이 어떤식으로 진행되려나..?
하고 찾아보니 createRef가 useRef로 변화하고 부모의 ref를 전달받는 방식으로 forwardref를 사용하는데는 변화가 없었다.
import { ChildModal } from "./ChildModal";
import { useParent } from "./useParent";
function App() {
const { modal, modalRef, toggleModal, handleOutsideClick } = useParent();
return (
<div className="page" onClick={() => handleOutsideClick()}>
<button type="button" onClick={() => toggleModal()}>
Open modal
</button>
{modal && <ChildModal ref={modalRef} toggleModal={toggleModal} />}
</div>
);
}
export default App;
import { useRef, useState } from "react";
export const useParent = () => {
const [modal, setModal] = useState(false);
const modalRef = useRef(null);
const toggleModal = () => {
setModal(!modal);
};
const handleOutsideClick = (e) => {
if (modalRef.current && !modalRef.current.contains(e?.target)) {
setModal(false);
}
};
return { modal, modalRef, toggleModal, handleOutsideClick };
};
import { forwardRef } from "react";
export const ChildModal = forwardRef((props, ref) => {
const { toggleModal } = props;
return (
<div className="modal" ref={ref}>
<button type="button" onClick={toggleModal}>
Close modal
</button>
</div>
);
});
forwardRef로 이뤄진 컴포넌트가 존재할 때만 적용하는 경우, 라이브러리 동작 방식을 변경하거나 리액트를 업데이트할 때 문제가 발생할 수 있다.
리액트 개발자도구에서 해당 컴포넌트를 확인하면 forwardRef로 표기되는 것을 확인할 수 있다. 이름이 없이 보이는 이유는 forwardRef 사용시 익명함수를 이용하기 때문인데, 이를 명확하게 알아보기 위해 다음과 같은 방법으로 이름을 지정해 줄 수 있다.
export const ChildModal = forwardRef(function modalName(props, ref) {
const { toggleModal } = props;
return (
<div className="modal" ref={ref}>
<p>모달 내부~~</p>
<button type="button" onClick={toggleModal}>
Close modal
</button>
</div>
);
});
export const ChildModal = forwardRef((props, ref) => {
// 기존 내용과 동일
});
ChildModal.displayName = "useDisplayName";
해당 속성은 부모의 DOM 노드를 가져와 사용하는 것이기 때문에 중첩해서 사용시 컴포넌트간 결합도가 높아지게 된다. 따라서 가장 말단 컴포넌트에 적용해 컴포넌트간의 결합도를 최소화하여 사용하는 것이 좋다.
참고
https://www.daleseo.com/react-forward-ref/
https://reactjs.org/docs/forwarding-refs.html#gatsby-focus-wrapper