
부모 -> 자식 방향 데이터 전달은 props를 통해 쉽게 가능합니다.
그보다 조금 더 복잡한 자식 -> 부모 방향 데이터 전달에 대해 알아보겠습니다.
구글링을 하다보니 함수 안에 함수를 두어 props로 넘기는 복잡한 구조도 있지만 이번에 소개할 hook은 useRef와 useImperativeHandle 입니다.
useRef는 아래와 같은 인터페이스를 갖고 있습니다.
interface MutableRefObject<T> {
current: T;
}
function useRef<T = undefined>(): MutableRefObject<T | undefined>;
useRef를 통해 변수를 선언하고 사용할 수 있습니다. 다만, useState와 다른 점은 렌더링 이후에도 기존 값을 가지고 있다는 점입니다.
변수에 할당된 값은 MutableRefObject 인터페이스를 따르는데 원하는 값을 current 에 할당하여 관리할 수 있습니다.
리액트에서는 리렌더링 되는 조건은 아래와 같습니다.
- Props의 변경
- State의 변경
- forceUpdate() 실행
- 부모 컴포넌트의 렌더링
react로 코딩하다보니 렌더링에 영향을 받지 않는 변수를 찾게 됩니다.
예를 들어, 렌더링 이전 값을 기억해야 하는 경우 혹은 자식 컴포넌트에서 부모 컴포넌트의 기능을 이용하는 경우 등 입니다.
import {useEffect, useRef} from "react";
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
위 코드는 useRef와 useEffect hook을 이용해서 만든 custom hook 입니다.
이 hook 을 이용하면 렌더링에 상관없이 값을 보존하고 특정 조건에만 변경하여 원하는 기능을 구현할 수 있습니다.
useImperativeHandle은 ref를 사용할 때 부모 컴포넌트에 노출되는 인스턴스 값을 사용자화합니다. 대부분의 경우 ref를 사용한 명령형 코드는 피해야 합니다. useImperativeHandle는 forwardRef와 더불어 사용해야 한다고 React 공식 홈페이지에 적혀있습니다.
useImperativeHandle의 인터페이스는 아래와 같습니다.
function useImperativeHandle<T, R extends T>(
ref: Ref<T> | undefined,
init: () => R,
deps?: DependencyList
): void;
첫 번째 인자인 ref는 Ref 객체를 받습니다.
두 번째 인자인 init은 함수를 받는데 이번 예제에서 부모 컴포넌트에서 쓰일 함수를 선언하고 내용을 채웁니다.
세 번째 인자인 deps는 useCallback의 deps처럼 deps 배열 안에 있는 변수의 상태가 바뀌면 useImperativeHandle이 재선언됩니다.
인터페이스만 보면 이해하기가 쉽지 않습니다.
쉽게 아래의 예제를 통해 이해하시면 좋겠습니다.
useImperativeHandle(ref, () => ({
handleNextStep () {
const updated: ReducerAutomationDto = update(singleAutomation, {
start: { $set: [ automation[0] ] },
end: { $set: [ automation[1] ] },
})
dispatch(controlAutomation(updated));
}
}))
위 예제는 Stepper인 부모 컴포넌트에서 다음 버튼을 클릭할 경우, 자식 컴포넌트에서 데이터를 store에 dispatch 하는 함수입니다.

저의 경우 Stepper 안에 Slider를 두었습니다. 다음 혹은 뒤로 갈 경우, 값이 그대로 남아있길 바랐기 때문에 Redux를 이용해야 했습니다.
Slider 컴포넌트에서 값이 변할 때마다 Redux에 저장을 해주어야 다음 혹은 뒤로 넘어가도 값이 보존될 수 있습니다. 하지만 이 방법은 리소스를 낭비하게 됩니다.
이 경우 useRef와 useImperativeHandle을 이용하면 좋습니다. 부모 컴포넌트인 Stepper에서 다음을 누를 경우만, 자식 컴포넌트인 Slider에서 값을 Redux에 저장할 수 있습니다.
결국 다음 혹은 뒤로를 클릭할 경우, Redux에 저장하는 로직으로 바꾸어 최적화하였습니다.
DOM 객체에 접근하기 위해 리액트는 ref를 만들어 사용합니다. 그래서 useRef는 렌더링에 영향을 받지 않고 변수를 관리할 수 있고 DOM 객체에 대한 직접적인 참조 주소를 반환 받아 HTML DOM 메소드 및 속성을 이용할 수 있습니다.
useImperativeHandle는 forwardRef와 사용 가능하며 부모는 자식의 DOM에 직접적으로 접근을 하는 것이 아니라 useImperativeHandle로 전달된 메서드에만 접근이 가능해집니다. 이로서 더욱 컴포넌트 간의 독립성을 보장할 수가 있습니다.