들어가며: 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)
'WEB > REACT' 카테고리의 다른 글
[☄️트러블슈팅] useEffect 내 setState 무한루프 (0) | 2023.10.05 |
---|---|
react-query를 짧게 사용해본 회고..랄까...? (0) | 2023.04.04 |
리액트에서 setState를 통해서 state를 변경해주는 이유 / setState의 비동기성 (0) | 2022.10.21 |
Redux (0) | 2021.07.13 |
[React] useEffect (0) | 2021.07.06 |