[DIP] ref as a prop

react

React v19 Stable 문서 확인 중 컴포넌트 prop으로 ref 전달 가능한 점을 확인하며, 이전 버전 컴포넌트에서는 전달되지 않는 이유가 궁금했다. 그리고 forwardRef는 이후 버전에서 사용되지 않고, 제거 예정인 것을 보고 forwardRef가 만들어진 이유와 제거하게 된 이유가 궁금하여 조사를 시작했다.

ref-as-a-prop-1



forwardRef 도입

React에서 함수형 컴포넌트가 주 사용으로 변경되며, 함수형 컴포넌트는 자체적인 인스턴스를 가지지 않기 때문에 ref 사용이 가능하도록 forwardRef를 도입하였다고 한다.


함수형 컴포넌트는 클래스 컴포넌트와는 다르게 단순 함수 호출로 동작하기 때문에 인스턴스를 가지지 않는다. 이는 간단한 컴포넌트 구현으로 확인 할 수 있었다. Typescript 파일로 처리 시 함수형 컴포넌트에 ref를 전달하면 타입 에러가 발생하여 전달 할 수 없고, Javascript에서도 전달 가능하지만 Warning이 발생한다.

jsx
class ClassComponent extends Component { render() { return <div>class</div>; } } const FuctionComponent = () => { return <div>function</div>; }; export default function App() { const classRef = useRef(); const funcRef = useRef(); useEffect(() => { console.log("class", classRef.current); // ClassComponent instance }, []); useEffect(() => { console.log("func", funcRef.current); // undefined }, []); return ( <div className="App"> <ClassComponent ref={classRef} /> <FuctionComponent ref={funcRef} /> </div> ); }


forwardRef는 내부적으로 함수형 컴포넌트를 감싸고, 함수형 컴포넌트가 props와 함께 ref를 전달받을 수 있도록 변경한다. 변경은 단순히 전달받은 함수형 컴포넌트를 “REACT_FORWARD_REF_TYPE” 타입을 가지는 객체로 변환하여 ref가 해당 객체를 참조하는 방식이다.

tsx
const REACT_FORWARD_REF_TYPE: symbol = Symbol.for('react.forward_ref'); export function forwardRef<Props, ElementType: React$ElementType>( render: ( props: Props, ref: React$RefSetter<React$ElementRef<ElementType>>, ) => React$Node, ) { ... const elementType = { $$typeof: REACT_FORWARD_REF_TYPE, render, }; return elementType; };


forwardRef render

forwardRef가 렌더링되는 과정은 함수형 컴포넌트를 가진 객체를 Render Phase 진행 시 createFiberFromTypeAndProps 에서 type과 props를 기반으로 새로운 Fiber 노드를 생성한다.

jsx
export function createFiberFromTypeAndProps( type: any, // React$ElementType key: null | string, pendingProps: any, owner: null | ReactComponentInfo | Fiber, mode: TypeOfMode, lanes: Lanes, ): Fiber { ... switch (type.$$typeof) { ... case REACT_FORWARD_REF_TYPE: fiberTag = ForwardRef; break getTag; ... } ... const fiber = createFiber(fiberTag, pendingProps, key, mode); fiber.elementType = type; fiber.type = resolvedType; fiber.lanes = lanes; return fiber; };

ref-as-a-prop-3



React v19 ref as a prop

jsx
function ReactElement( type, key, self, source, owner, props, debugStack, debugTask, ) { ... const refProp = props.ref; ... element = { $$typeof: REACT_ELEMENT_TYPE, type, key, ref, props, }; ... return element; }

여기서 React v19에서 함수형 컴포넌트로 전달된 ref는 prop으로 전달된 ref를 element 객체에 연결하지만, 단순히 prop으로 전달할 뿐 함수형 컴포넌트 객체 자체를 참조하는 것이 아니기 때문에 수동으로 참조 할당을 지정해 주어야 한다.

실제로 위에서 사용된 코드를 React v19 버전에서 실행하더라도 워닝 및 에러는 발생하지 않지만 funcRef는 undefined로 찍히는 것을 확인 할 수 있다.


함수형 컴포넌트에서 ref에 대한 수동으로 참조 할당 처리는 useImperativeHandle 훅을 사용하는 방식과 직접 JSX로 작성된 컴포넌트에 지정해야 한다.

jsx
import { useRef, useImperativeHandle } from "react"; function MyInput({ ref }) { const inputRef = useRef(null); useImperativeHandle( ref, () => { return { focus() { inputRef.current.focus(); }, scrollIntoView() { inputRef.current.scrollIntoView(); }, }; }, [] ); return <input ref={inputRef} />; }


forwardRef deprecate and remove

React v19에서 함수형 컴포넌트로 ref 전달이 가능해지며 forwardRef가 더 이상 필요하지 않게 되어 향후 버전에서는 지원 중단 예정이라고 한다.

forwardRef가 사용되지 않음으로써 이후에는 ref 전달을 위해 함수형 컴포넌트를 forwardRef로 감싸는 불필요(?)한 보일러플레이트 같은 추가적인 코드가 줄어들어 코드 보기가 편해질 것으로 예상된다.

React v19로 마이그레이션을 진행하는 경우 forwardRef를 제거하는 작업도 미리 해두는 것이 향후 forwardRef가 더 이상 지원되지 않는 버전으로 마이그레이션 하더라도 문제없이 프로젝트가 동작할 것 같다.



In conclusion

ref-as-a-prop-2

해당 부분을 알아보며 들었던 개인적인 생각은, React를 사용해 본 프론트 개발자라면 React를 개발하는 개발자분들은 아마도 엄청 기술력이 뛰어난 똑똑한 개발자라는 생각을 해본 적이 있을 것이다. (나만의 생각일 수도…) 이런 똑똑한 개발자분들도 멋진 아이디어와 깊은 고민을 하고 React를 개발하였지만, 이후에 더 좋은 사용성 혹은 방향성에 따라 이전에 개발하였던 코드를 폐기하고 수정을 한다. 이러한 과정들을 보며 우리 또한 그 상황에 맞는 최선, 최적의 코드를 만들어내고 이후에 예전 코드를 보며 부끄러워하며 수정하는 과정을 비슷하게 수행하는 것 같다.

예전 코드를 보며 ‘왜 그때 이렇게 짰을까?’ 하고 부끄러워하면서도 수정하는 과정 자체가 우리의 성장을 보여주는 증거가 아닐지 생각해 본다.