여러 개의 React Ref 병합하기

박세영·2024년 11월 28일
1

React

목록 보기
1/1

동일한 dom element를 제어하고 싶어하는 여러 개의 ref를 잘 통합하는 방법에 대한 아티클입니다.

배경

TextArea라는 이름의 공통 컴포넌트가 있습니다. 이 컴포넌트는 일반적인 textarea 요소와 거의 동일한 동작을 하지만 두 개의 특별한 요구사항이 있습니다.

  1. 텍스트 입력 양에 따라 동적으로 textarea 높이가 늘어난다.
  2. fowardRef를 통해 외부 주입된 ref를 지원한다.

1번 요구사항을 만족 시키기 위해 innerRef라는 이름의 ref를 생성해서 텍스트 양에 따라 동적으로 높이를 조절하도록 했습니다.

2번 요구사항 역시 만족 시키기 위해 컴포넌트를 forwardRef로 감싸고 forwardedRef를 인자로 받았습니다.

두 개의 ref를 이제 textarea에 연결 시켜 주기만 하면 됩니다. 하지만 아쉽게도 dom element는 ref를 하나 밖에 받지 못합니다.

둘 중 하나만 쓰자

처음 떠올린 방법은 forwardedRef가 있으면 해당 ref를 사용하고, 없다면 TextArea 컴포넌트 내에서 선언한 innerRef를 사용하는 방식입니다.

const ref = forwardedRef ?? innerRef;

처음에는 깔끔한 방법이라 생각되었지만 아래 ref.current를 사용한 부분에서 에러가 발생했습니다.

ref.current를 사용한 경우 발생하는 에러 메시지

Property 'current' does not exist on type '((instance: HTMLTextAreaElement | null) => void) | MutableRefObject<HTMLTextAreaElement | null>'.

Property 'current' does not exist on type '(instance: HTMLTextAreaElement | null) => void'.ts(2339)

에러는 forwardedRefCabllackRef인 경우가 고려되지 않아서 발생한 것이었습니다.

type Ref<T> = RefCallback<T> | RefObject<T> | null;

useImperativeHandle()을 활용한 방식

ReactuseImerpativeHandle api는 포워딩된 ref의 동작을 명령적으로 선언할 때 사용됩니다. 사용법은 아래와 같습니다.

    useImperativeHandle(forwardedRef, () => ({
      focus: () => innerRef.current?.focus(),
      get scrollHeight() {
        return innerRef.current?.scrollHeight;
      },
    }));

forwardedRef을 통해 수행하고 싶은 동작을 미리 선언하고 노출하기 때문에 안정적인 방식으로 보입니다. 하지만 forwardedRef의 동작을 일일이 지정 해주어야 하기 때문에, dom element의 모든 native method, property를 그대로 포워딩 시켜주는 것은 매우 번거로워 보입니다.

merge ref

결국 여러개의 ref를 병합해서 하나의 dom element에 달아줄 수 있다면 가장 깔끔한 해답이 될 것 같습니다.

CallbackRef를 활용하면 여러 개의 ref를 병합할 수 있습니다.

  const mergeRefs = (node: HTMLTextAreaElement | null) => {
    if (typeof forwardedRef === 'function') {
      forwardedRef(node); // Callback Ref 처리
    } else if (forwardedRef) {
      (forwardedRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node; // Object Ref 처리
    }
    innerRef.current = node; // 내부 Ref 설정
  };
  
	return <textarea ref={mergeRef()} />

이제 innerRefforwardedRef를 모두 textarea에 연결해 요구사항을 만족할 수 있게 되었습니다!

더 나아가 다양한 형태의 ref를 여러 개 병합할 수 있도록 개선해보겠습니다.

mergeRefs 함수 일반화 하기

mergeRefs 함수는 여러 개의 다양한 ref를 인자로 받아 dom에 등록 시킵니다.

import type { MutableRefObject, Ref, RefCallback } from 'react';

export const mergeRefs = <T>(
  ...inputRefs: (Ref<T> | undefined)[]
): Ref<T> | RefCallback<T> => {
  const notNilRefs = inputRefs.filter((inputRef) => !!inputRef);

  return (node) => {
    notNilRefs.forEach((inputRef) => {
      if (typeof inputRef === 'function') inputRef(node);
      else (inputRef as MutableRefObject<T | null>).current = node;
    });
  };
};
<textarea ref={mergeRefs(forwardedRef, innerRef)} />

0개의 댓글