React는 왜 props로 ref를 전달하지 못할까?

JJ·2024년 7월 29일
0

React는 왜 props로 ref를 전달하지 못할까?에 대한 궁금증이 생겼고 Deep Dive 해보기로 했다.

React의 핵심 원칙은 캡슐화와 추상화

React는 컴포넌트 기반 라이브러리이다.
컴포넌트 단위로 내부 구현을 캡슐화하고 명확한 인터페이스를 제공하기 위해 추상화를 고려하여 설계하여야한다.

프론트엔드는 개발 패러다임이 자주 바뀌기 때문에 코드 폐기 또는 변경사항에 있어서 유연하게 대처할 수 있어야하고, 그러기 위해서는 다양한 요소들을 적절하게 나누고 추상화를 해야한다.

ref는 DOM노드나 컴포넌트 인스턴스에 직접적인 접근을 제공한다. 만약 이 구현을 props로 자유롭게 전달할 수 있다면, 부모컴포넌트가 자식 컴포넌트의 내부구현에 과도하게 의존할 위험이 있다. 이 행위는 컴포넌트의 캡슐화를 깨트리고 컴포넌트간 결합도를 높이게 된다.

// 부모 컴포넌트
const ParentComponent = () => {
  const inputRef = useRef(null);

  useEffect(() => {
    // 자식 컴포넌트의 내부 구현에 직접 접근
    if (inputRef.current) {
      inputRef.current.style.border = '2px solid red';
      inputRef.current.focus();
    }
  }, []);

  return <ChildComponent inputRef={inputRef} />;
};

// 자식 컴포넌트
const ChildComponent = ({ inputRef }) => {
  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Enter text" />
      <button>Submit</button>
    </div>
  );
};

위 코드는 부모 컴포넌트가 자식컴포넌트의 내부구현에 직접적으로 접근하고 있다. 또한 부모 컴포넌트는 자식컴포넌트가 내부적으로 input요소를 사용한다는 것을 알고 있어야 한다. 이는 자식 컴포넌트의 세부 구현사항을 노출시키는 행위이다.

그리고 자식컴포넌트의 내부 구조가 변경된다면 (input -> textarea) 부모 컴포넌트 구현도 수정해야한다.

또한 이렇게 구현된 자식컴포넌트는 재사용하기 힘들다. 항상 inputRef를 전달받아야하기 때문이다.

단방향 데이터 흐름 유지

React의 또 다른 중요한 특징은 단방향 데이터 흐름이다.
단방향 데이터 흐름은 부모 컴포넌트의 상태가 자식컴포넌트로 흐르는 것을 의미한다.
ref를 props로 전달하게 되면, 이 원칙이 흐려질 수 있다.
자식 컴포넌트가 부모로부터 받은 ref를 통해 부모의 DOM을 직접 조작할 수 있게 되어, 양방향 바인딩과 유사한 상황이 발생할 수 있다.

예측 가능성과 디버깅 용이성

React의 선언적 프로그래밍 모델은 애플리케이션의 상태와 UI를 예측 가능하게 만든다.
그러나 ref를 통한 직접적인 DOM 조작은 이러한 모델과 충돌할 수 있다.
ref를 통해 컴포넌트 외부에서 DOM을 직접 조작하게 되면, React의 렌더링 사이클을 우회하게 된다.
이는 애플리케이션의 동작을 예측하기 어렵게 만들고, 테스트나 디버깅을 복잡하게 만들 수 있다.

import React, { useRef, useEffect, useState } from 'react';

const UnpredictableComponent = () => {
  const [count, setCount] = useState(0);
  const buttonRef = useRef(null);

  useEffect(() => {
    // React의 렌더링 사이클 외부에서 DOM을 직접 조작
    const button = buttonRef.current;
    if (button) {
      button.addEventListener('click', () => {
        button.textContent = `Clicked ${count + 1} times`;
        // 주의: 여기서 상태를 직접 변경하지 않음
      });
    }

    // 클린업 함수
    return () => {
      if (button) {
        button.removeEventListener('click', () => {});
      }
    };
  }, []); // 빈 의존성 배열

  const handleClick = () => {
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button ref={buttonRef} onClick={handleClick}>
        Click me
      </button>
    </div>
  );
};

export default UnpredictableComponent;

위 코드처럼 ref를 통한 직접적인 DOM 조작은 예측 가능성을 해치고 디버깅을 매우 어렵게 만든다.

1. 비일관적인 UI 상태

  • 현재 버튼 텍스트의 콘텐츠는 React 상태(count)와 동일하지 않다.
  • 사용자가 버튼을 클릭하게되면 버튼의 텍스트는 변경되지만 실제 count상태와 불일치하게 된다.

2. 예측 불가능한 동작

  • React의 가상 DOM 갱신과 실제 DOM 조작이 충돌할 수 있다.

위 문제를 해결하기 위해 React의 선언형 UI를 따르는 방식으로 아래처럼 리팩토링 할 수 있다.

import React, { useState } from 'react';

const PredictableComponent = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>
        Clicked {count} times
      </button>
    </div>
  );
};

export default PredictableComponent;

성능 최적화

React는 가상 DOM과 다양한 렌더링 최적화 기법을 사용하여 성능을 향상시킵니다. ref를 통한 직접적인 DOM 조작은 이러한 최적화 과정을 무력화시킬 수 있다.
React는 상태 변화에 따라 효율적으로 UI를 업데이트하도록 설계되었다. 그러나 ref를 통한 직접적인 DOM 조작은 React가 이러한 변화를 추적하기 어렵게 만들어, 불필요한 리렌더링이 발생하거나 UI 불일치가 발생할 수 있다.

그렇다면 ref가 필요할때는? forwardRef

물론 ref를 사용해아하는 상황이 생길 수 있다.
이를 위해 react에서는 forwardRef라는 API를 제공한다.
이를 통해 개발자는 필요한 경우 ref를 명시적이고 제어된 방식으로 전달할 수 있다.

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

참고

https://fe-developers.kakaoent.com/2022/221020-component-abstraction/
https://ko.react.dev/reference/react/useRef![](https://velog.velcdn.com/images/ckdwns9121/post/78fed96b-418d-467f-8009-dbf7c4bc4a39/image.png)

0개의 댓글