본문 바로가기
WEB/REACT

Ref를 좀 더 잘 써볼까? (forwardRef와 useImperativeHandle)

by mingzoo 2024. 6. 28.

들어가며: React의 Ref 전달 문제

 

React의 컴포넌트에서 ref를 전달하려고 하면 문제가 발생한다.

// 🚨 동작하지 않는 코드
const Input = (props) => {
  return <input ref={props.inputRef} />;
};

// 부모 컴포넌트
const Parent = () => {
  const inputRef = useRef(null);
  return <Input inputRef={inputRef} />; // ref가 전달되지 않음
};



이는 React가 기본적으로 ref를 일반 prop으로 전달하지 않기 때문입니다. 이런 제한이 있는 이유는:
1. 캡슐화 유지: 내부 구현 세부사항을 보호
2. 예측 가능한 데이터 흐름 보장
3. 불필요한 노출 방지

 

forwardRef: React의 공식적인 ref 전달 메커니즘


React 공식 문서에 따르면, `forwardRef`는 다음과 같은 상황에서 특히 유용합니다:

1. DOM 요소를 부모 컴포넌트에 노출해야 할 때
2. 고차 컴포넌트(HOC)에서 ref를 전달할 때
3. 재사용 가능한 컴포넌트 라이브러리 개발 시

// ✅ forwardRef를 사용한 올바른 구현
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
  return <input ref={ref} {...props} />;
});

// 부모 컴포넌트
const Parent = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  
  useEffect(() => {
    inputRef.current?.focus();
  }, []);
  
  return <Input ref={inputRef} />;
};



### forwardRef의 타입스크립트 활용

// 명확한 타입 정의로 안전성 확보
interface InputProps {
  label?: string;
  onChange?: (value: string) => void;
}

const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
  const { label, onChange, ...rest } = props;
  
  return (
    <div>
      {label && <label>{label}</label>}
      <input
        ref={ref}
        onChange={e => onChange?.(e.target.value)}
        {...rest}
      />
    </div>
  );
});



useImperativeHandle

`useImperativeHandle`은 ref를 통해 노출할 값이나 메서드를 세밀하게 제어할 수 있게 해주는 강력한 Hook입니다.

1. 기본 사용법

interface CustomInputHandle {
  focus: () => void;
  getValue: () => string;
  setValue: (value: string) => void;
}

const CustomInput = forwardRef<CustomInputHandle, InputProps>((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);
  const [value, setValue] = useState('');

  useImperativeHandle(ref, () => ({
    // 커스텀 메서드 정의
    focus: () => inputRef.current?.focus(),
    getValue: () => value,
    setValue: (newValue) => setValue(newValue)
  }), [value]); // 의존성 배열 주의

  return <input ref={inputRef} value={value} onChange={e => setValue(e.target.value)} />;
});

 


### 2. 실전 활용 패턴

#### 2.1 선택적 메서드 노출

```typescript
interface GridHandle {
  // 필수 메서드
  required: {
    refresh: () => void;
    clear: () => void;
  };
  // 선택적 메서드
  optional?: {
    export?: () => void;
    print?: () => void;
  };
}

const Grid = forwardRef<GridHandle, Props>((props, ref) => {
  useImperativeHandle(ref, () => ({
    required: {
      refresh: () => { /* 구현 */ },
      clear: () => { /* 구현 */ }
    },
    optional: props.enableExport ? {
      export: () => { /* 구현 */ }
    } : undefined
  }));
});
```

#### 2.2 성능 최적화

```typescript
const Grid = forwardRef((props, ref) => {
  const methodsRef = useRef({
    refresh: () => { /* 구현 */ },
    clear: () => { /* 구현 */ }
  });

  // 메서드 재생성 방지
  useImperativeHandle(ref, () => methodsRef.current, []);
});
```

#### 2.3 조건부 기능 노출

```typescript
const Form = forwardRef<FormHandle, FormProps>((props, ref) => {
  const { mode = 'basic' } = props;

  useImperativeHandle(ref, () => ({
    // 기본 기능
    submit: () => { /* 구현 */ },
    reset: () => { /* 구현 */ },
    
    // 고급 기능
    ...(mode === 'advanced' && {
      validate: () => { /* 구현 */ },
      transform: () => { /* 구현 */ }
    })
  }), [mode]);
});
```

### 3. useImperativeHandle의 주의사항

1. **의존성 배열 관리**
```typescript
// 🚨 잘못된 사용
useImperativeHandle(ref, () => ({
  getData: () => data // data가 의존성 배열에 없음
})); 

// ✅ 올바른 사용
useImperativeHandle(ref, () => ({
  getData: () => data
}), [data]); // data를 의존성 배열에 포함
```

2. **메모리 누수 방지**
```typescript
const Component = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => {
    const heavyResource = createExpensiveResource();
    
    return {
      useResource: () => heavyResource
    };
  }, []); // 리소스가 한 번만 생성되도록 함
  
  // cleanup 로직 추가
  useEffect(() => {
    return () => {
      // 리소스 정리
    };
  }, []);
});
```

## 실전 사례: RealGrid에서의 활용

RealGrid 컴포넌트에서는 이러한 패턴들을 복합적으로 활용합니다:

```typescript
const RealGrid = forwardRef<RealGridRef, RealGridProps>((props, ref) => {
  const gridRef = useRef<HTMLDivElement>(null);
  const [isReady, setIsReady] = useState(false);
  
  useImperativeHandle(ref, () => ({
    // DOM 요소 접근
    grid: gridRef.current,
    
    // 상태 확인
    isReady,
    
    // 데이터 조작 메서드
    getChangedRows: <T = any>() => {
      if (!isReady) return null;
      return /* 구현 */;
    },
    
    // 유효성 검사
    validateRequireColumns: (fields?: string[]) => {
      if (!isReady) return false;
      return /* 구현 */;
    },
    
    // 액션 메서드
    commitChange: () => {
      if (!isReady) return;
      /* 구현 */
    }
  }), [isReady]);
  
  return <div ref={gridRef}>/* 구현 */</div>;
});
```

## Best Practices 및 권장 패턴

1. **명시적 타입 정의**
   ```typescript
   interface ComponentRef {
     method1: () => void;
     method2: (param: string) => boolean;
   }
   
   const Component = forwardRef<ComponentRef, Props>((props, ref) => {
     // 구현
   });
   ```

2. **안전한 메서드 호출**
   ```typescript
   const Parent = () => {
     const componentRef = useRef<ComponentRef>(null);
     
     const handleClick = () => {
       // 옵셔널 체이닝으로 안전하게 호출
       componentRef.current?.method1();
     };
   };
   ```

3. **적절한 캡슐화**
   ```typescript
   interface PublicAPI {
     // 외부에 노출할 메서드만 정의
   }
   
   interface PrivateAPI extends PublicAPI {
     // 내부 구현에 필요한 추가 메서드
   }
   ```

## 마치며

`forwardRef`와 `useImperativeHandle`은 React의 단방향 데이터 흐름 원칙을 해치지 않으면서도, 필요할 때 컴포넌트 간의 명령형 인터페이스를 제공할 수 있게 해주는 강력한 도구입니다. 하지만 이런 기능들은 꼭 필요한 경우에만 신중하게 사용해야 합니다.

---
참고 자료:
- [React 공식 문서 - forwardRef](https://react.dev/reference/react/forwardRef)
- [React 공식 문서 - useImperativeHandle](https://react.dev/reference/react/useImperativeHandle)

728x90