useImperativeHandle(ref, createHandle, [deps])
Docs
useImperativeHandle은 ref를 사용할 때 부모 컴포넌트에 노출되는 인스턴스 값을 사용자화(customizes)합니다. 항상 그렇듯이, 대부분의 경우 ref를 사용한 명령형 코드는 피해야 합니다. useImperativeHandle는 forwardRef와 더불어 사용하세요.
useImperativeHandle
훅은 부모 컴포넌트에서 자식 컴포넌트의 State를 관리해야할 경우 사용할 수 있는 훅입니다.
즉 자식의 상태변경을 부모가 하거나, 자식 컴포넌트의 State 핸들러를 부모에서 호출하는 경우 사용할 수 있습니다
이 훅의 단순한 사용예시는 공식문서와 여러 블로그에 잘 나와있어요.
useImperativeHandle 떠먹여드립니다.
이 글에서는 아래와 같은 기능 차트 기능개발 도중 제가 겪은 문제를 해결하는 과정에 대해 설명드리겠습니다!
위 차트 상의 라인을 클릭하면 아래 처럼 동작하여야 합니다.
function Parent() {
const chartData = [....];
const [tableData, setTableData] = useState();
return (
<Container>
<Chart
data={chartData}
setTableData={setTableData}
/>
...
{showTable
? <Table data={tableData} />
: <OtherComp />
}
</Container>
)
}
function Chart({ data, setTableData}) {
const [showTooltip, setShowTooltip] = useState();
return (
<LineChart>
<Line onClick={(d) => {
setShowTooltip(true)};
setTableData(d);
}/>
...
{showTooltip && <Tooltip />}
</LineChart>
)
}
실제 코드를 단순화한 예시입니다.
우선 차트와 테이블을 감싸고 있는 부모컴포넌트가 있구요
라인 차트를 클릭했을때 tooltip을 활성화하고, 부모에서 관리되고 있는 tableData 상태를 업데이트 해줍니다
아래와 같은 컴포넌트 구조에서 테이블의 x 버튼을 눌렀을때 차트 컴포넌트의 툴팁도 함께 닫으려면 어떻게 해야 할까요?
가장 단순한 해결책은 차트 내부의 showTooltip 상태를 부모로 끌어올려서 관리하는 방법입니다.
하지만 이렇게 했을때 맘에 안드는 부분이 있습니다
Tooltip은 차트 데이터를 이용하고 있으며 UI상 차트에 종속 되어있습니다. 단순 close를 하기 위해 tooltip의 상태를 상위로 끌어올리면 props가 추가되면서 상태가 복잡해지고 관심사가 뒤섞여 이해하기 어려운 코드가 되어버리죠.
물론 이 경우 상태관리 라이브러리는 좋은 대안이 될 것 같습니다. 하지만 상태 공유 라이브러리를 사용하지 않는 프로젝트라면, 이 기능을 위해 상태 공유 라이브러리를 도입하는건 무리가 있습니다. 또한 Context API는 불필요한 리렌더링 방지를 위한 최적화 작업까지 필요하여 번거롭다고 느껴집니다.
따라서 해결해야될 문제를 단순하게 정의하면 아래와 같아요
state를 끌어올리지 않고, 상태관리 라이브러리나 Context API 를 사용하지 않으면서 Chart 컴포넌트의 showTooltip 상태를 부모에서 관리하기
이 문제를 useImperativeHandle 과 forwarRef를 해결할 수 있었습니다
코드 먼저 살펴볼게요
function Parent() {
const chartData = [....];
const chartRef = useRef(); // ref 선언
const [tableData, setTableData] = useState();
return (
<Container>
<Chart
ref={chartRef}
data={chartData}
setTableData={setTableData}
/>
<Table
data={tableData}
onClickClose={() => {
chartRef.current?.onCloseTooltip(); // 자식 컴포넌트에서 전달받은 메소드를 실행합니다.
}}
/>
</Container>
)
}
const Chart = forwardRef(({ data, setTableData }, chartRef) => {
const [showTooltip, setShowTooltip] = useState(false);
// hook을 이용하여 Parent에서 사용할 메소드를 선언해줍니다
useImperativeHandle(chartRef, () => ({
onCloseTooltip: () => {
setShowTooltip(false);
}
}))
return (
<LineChart>
<Line onClick={(d) => {
setShowTooltip(true)};
setTableData(d);
}/>
...
{showTooltip && <Tooltip />}
</LineChart>
)
})
먼저 ref를 하위컴포넌트로 전달하기 위해서 자식 컴포넌트를 forwardRef로 감싸줍니다.
다음, 자식 컴포넌트 안에서 useImperativeHandle을 사용하면 되는데, 첫번째 인자로 전달받은 ref를 넣어줍니다. 그리고 두번째 인자로는 객체를 리턴하는 함수를 넣어줍니다.
여기서 두번째 인자의 함수 리턴값에 변수나 메소드를 넣어주면 부모컴포넌트에서 ref.current 를 통해 접근 가능해집니다.
저의 경우 onClickCloseTooltip에 setState 함수를 넣어줘서, 부모 컴포넌트에서 tooltip을 닫는 함수를 호출할 수 있게 되었습니다.
state를 끌어올릴 경우 불필요하게 부모컴포넌트까지 리렌더링 되는 문제도 덤으로 해결됩니다 :)
useImperativeHandle 와 forwardRef를 활용하면 state 끌어올리거나 상태관리 툴을 사용하지 않고서도 부모가 자식컴포넌트의 상태를 관리할 수 있으며, 부모와 자식 컴포넌트간의 관심사 분리를 유지할 수 있습니다!
위 예시에서는 단순히 tooltip을 close 하는 기능을 위해 useImperativeHook을 사용했지만, 조금더 복잡하고 다양한 메소드 혹은 state 들을 정의해서 부모에게 전달해줘야 할 경우 더 유용할 것 같네요.
하지만 공식문서에 나와있듯 가능한 ref를 사용한 명령형 코드는 피하는 것이 좋습니다.
( 아마 렌더링 이후 DOM에 직접 접근하게 되고, 리액트의 선언형 문법과 괴리가 생길 수 있기 때문이지 않을까 생각해봅니다 )
따라서 관심사의 분리 or 가독성 등 명백히 이점이 있을때 useImpertative를 사용하는 것이 좋을것같습니다