I code, therefore I exist.

웹 프론트 엔드 개발을 공부하고 있는 Ocean이라고 합니다. 만나서 반갑습니다.

WEB/REACT

forwardRef와 useImperativeHandle

Ocean 2024. 12. 22. 22:06

forwardRef useImperativeHandle 정리

안녕하세요, Ocean입니다.

입사 준비를 하면서, 그동안 잘 알지 못했던 React API와 Hooks에 대해 정리하고 있습니다. 이번에는 비교적 간단하면서도 서로 연관성이 높은 forwardRefuseImperativeHandle에 대해 알아보려고 합니다.

이 두 가지는 컴포넌트의 명령형(Imperative) 접근 방식을 다룰 때 유용하며, React의 선언적 데이터 흐름과 조화를 이루도록 설계되었습니다. 이 주제를 간결하고 명확하게 정리해보겠습니다. 😊

참고 글

1. forwardRef - React

2. useImperativeHandle - React


forwardRef

forwardRef는 React에서 컴포넌트가 부모로부터 전달된 ref를 받아 내부의 DOM 노드나 하위 컴포넌트에 연결할 수 있도록 하는 기능입니다. 이를 통해 부모 컴포넌트가 자식 컴포넌트 내부의 DOM 노드에 직접 접근할 수 있게 됩니다.

React 공식 문서에서는 이를 "컴포넌트가 부모 컴포넌트에게 DOM 노드를 노출한다"고 표현합니다.

사용법

컴포넌트가 ref를 상위에서 전달 받고싶다면 해당 컴포넌트를 forwardRef로 감싸면 됩니다.

import { forwardRef } from 'react';

const Section = forwardRef(function(title, ref) {
  return (
    <section ref={ref} style={{ height: '100vh' }}>
      <h1>{title}</h1>
    </section>
  );
});

export default Section;


위와 같이 forwardRef를 호출하며 매개변수로 컴포넌트를 넘깁니다. 이 때 컴포넌트는 ref를 전달 받습니다.

import { useRef } from 'react';
import Section from './Section';

function App() {
  const homeRef = useRef<HTMLElement | null>(null);
  const aboutRef = useRef<HTMLElement | null>(null);
  const historyRef = useRef<HTMLElement | null>(null);

  const handleClick = (section: 'home' | 'about' | 'history') => {
    switch (section) {
      case 'home':
        homeRef.current?.scrollIntoView({ behavior: 'smooth' });
        break;
      case 'about':
        aboutRef.current?.scrollIntoView({ behavior: 'smooth' });
        break;
      case 'history':
        historyRef.current?.scrollIntoView({ behavior: 'smooth' });
        break;
      default:
        break;
    }
  };

  return (
    <>
      <nav>
        <button onClick={() => handleClick('home')}>Go to Home</button>
        <button onClick={() => handleClick('about')}>Go to About</button>
        <button onClick={() => handleClick('history')}>Go to History</button>
      </nav>
      <Section title="home" ref={homeRef} />
      <Section title="about" ref={aboutRef} />
      <Section title="history" ref={historyRef} />
    </>
  );
}

export default App;


이로써 App 컴포넌트의 ref를 Section으로 전달하여, Section의 DOM을 부모 컴포넌트에서 조작할 수 있게 되었습니다!
앞서 말했듯이, 이를 React에서는 DOM 노드를 노출한다고 표현합니다.


DOM 노드를 외부로 노출하는 것의 한계

React는 선언적 데이터 흐름을 중심으로 설계되었습니다. 즉, 컴포넌트는 외부에서 전달받은 props와 내부 state에 따라 UI를 그리며, 외부에서는 컴포넌트의 내부 동작을 직접 조작하지 않는 것을 권장합니다.

forwardRef는 DOM 노드를 외부에 노출함으로써 React의 추상화를 일부 깨뜨릴 수 있게 됩니다. 이는 다음과 같은 문제를 초래할 수 있습니다. 

  1. 캡슐화 손실: 자식 컴포넌트의 내부 로직이 외부(부모)에 노출됩니다.
  2. 결합도 증가: 부모가 자식의 구현 세부사항에 의존하게 됩니다. (순수성 상실)
  3. 변경이 어려움: 자식 컴포넌트의 구조 변경 시 부모 컴포넌트도 수정해야 합니다.

따라서 forwardRef는 DOM 노드에 직접 접근해야 하는 특정 작업과 같은 정말 필요한 경우에만 사용해야 합니다.


forwardRef와 제약 조건: useImperativeHandle

앞서 설명한 대로 forwardRef는 부모 컴포넌트에서 자식 컴포넌트의 DOM 노드나 내부 동작에 직접 접근할 수 있게 해줍니다. 하지만 이렇게 하면 컴포넌트의 모든 DOM 노드와 내부 세부사항이 외부에 노출됩니다.

React는 이를 해결하기 위해, React는 useImperativeHandle이라는 Hook을 제공합니다. 이를 사용하면 부모에게 노출할 메서드만 제한적으로 제공할 수 있습니다.

사용법

import { forwardRef, useImperativeHandle, useRef } from 'react';

const Section = forwardRef(function ({ title }, ref) {
  const sectionRef = useRef(null);
  // useImperativeHandle로 노출 범위를 제한
  useImperativeHandle(ref, () => ({
    scrollToSection: () => {
      sectionRef.current?.scrollIntoView({ behavior: 'smooth' });
    },
  }));

  return (
    <section ref={sectionRef} style={{ height: '100vh' }}>
      <h1>{title}</h1>
    </section>
  );
});

export default Section;

 

useImperativeHandle을 사용하면 부모에게 명령형(Imperative)으로 호출할 메서드를 명시적으로 정의할 수 있습니다. 이 방식으로 DOM 노드의 모든 메서드를 외부에 노출하지 않고 필요한 동작만 제공할 수 있습니다.

다른 Hooks와 마찬가지로 반드시 컴포넌트 최상위에서 호출해야 하며, 반드시 forwardRef와 조합해 사용합니다.

매개변수

  • ref: forwardRef로 전달받은 두 번째 인자입니다.
  • createHandle: 부모에게 노출할 메서드를 정의하는 함수입니다. 이 함수에서 반환한 객체만 부모에서 사용할 수 있습니다.
  • dependencies (선택 사항): createHandle에서 참조하는 반응형 값(예: state나 props)을 나열합니다.

위 예제에서는 scrollToSection 메서드만 부모에 호출되며, DOM 노드의 기본 메서드인 focus는 사용할 수 없습니다.

const handleClick = (section) => {
  switch (section) {
    case 'home':
      homeRef.current?.scrollToSection();
      homeRef.current?.focus(); // 에러: TypeError: homeRef.current.focus is not a function
      break;
    case 'about':
      aboutRef.current?.scrollToSection();
      break;
    case 'history':
      historyRef.current?.scrollToSection();
      break;
    default:
      break;
  }
};

 


정리

forwardRefuseImperativeHandle은 명령형 프로그래밍이 필요한 경우 유용하지만, React의 선언적 데이터 흐름을 깨지 않도록 주의해야 합니다.
이 때, useImperativeHandle을 활용해 외부에 노출할 동작을 제한하면 캡슐화를 유지하면서 명령형 접근을 제공할 수 있습니다. 이 방식을 통해 컴포넌트 간 결합도를 낮추고 유지보수성을 높일 수 있습니다.

'WEB > REACT' 카테고리의 다른 글

React의 의존성 주입  (1) 2025.03.27
useState와 클로저  (2) 2024.12.12
useReducer  (1) 2024.06.29
useContext  (0) 2024.06.19
useMemo  (0) 2024.06.19