Ref란 무엇일까?
- Ref는 render 메서드에서 생성된 DOM 노드나 React 엘리먼트에 접근하는 하나의 방식이다.
Ref는 그럼 언제 사용할까?
- 포커스, 텍스트 선택 영역, 혹은 미디어의 재생 관리
- 애니메이션 직접적으로 실행시킬 때
- 서드 파티 DOM 라이브러리를 React 와 같이 사용할 때
라고 공식문서에 나와 있다.
하지만 나는 이것을 보고 정확히 무엇인지? 이해가 되지 않았다.
여기서 나는 React의 렌더링 조건이 생각난다.
- props가 변할 때
- state가 변할 때
- 부모 컴포넌트가 리렌더링 될 때
React는 state가 바뀌면 렌더링이 되는 반면, ref는 DOM에 직접 접근하기 때문에 렌더링이 되지 않는다!
그래서 나는 Ref를 보통, 불 필요한 렌더링을 막기 위해서 사용하곤 한다.
- DOM에 직접 접근
- 상태 변경 값 없이 값 가져오기
- 애니메이션
이 주로 이용될 수 있다고 생각한다.
forwardRef에 대해 알아보자
React에서는 ref를 사용하여 DOM 요소 또는 컴포넌트 인스턴스에 직접 접근이 가능한데, 부모 컴포넌트에서 자식 컴포넌트에게 state를 전달하여 props를 받는 반면 ref를 전달하고 싶을 땐 바로 forwardRef를 이용한다 !
즉 부모 컴포넌트에서 생성된 ref를 자식 컴포넌트에게 전달하며, 자식 컴포넌트는 부모 컴포넌트와 자식 컴포넌트의 인스턴스에 접근할 수 있다!
forwardRef는 자식 컴포넌트 내부 구조와 동작을 부모 컴포넌트에 노출시켜, 컴포넌트 간의 강한 결합을 초래할 수 있다.
자식 컴포넌트가 변경되면 부모 컴포넌트도 함께 변경될 수 있어
useImperativeHandle 훅을 react에서 제공한다
useImperativeHandle가 뭔데?
부모 컴포넌트로 노출되는 인스턴스 값을 직접 커스터마이징할 수 있게 해준다.
이를 사용하면 자식 컴포넌트는 필요한 메소드와 값을 선택적으로 부모 컴포넌트에 노출시킬 수 있다.
자식 컴포넌트는 필요한 메소드와 값을 선택적으로 부모 컴포넌트에 노출이란 무슨 뜻일까?
나는 디자인 시스템을 개발할때 input같은 경우 불필요한 렌더링 방지를 위해 ref를 이용하여 값을 이용하곤 했다.
const Input = forwardRef<HTMLInputElement, Props>(({
label, disabled, type ...}, ref) => {
**useImperativeHandle(ref, () => inputRef.current);**
return (
<Container>
<StyledInput type={type} disabled={disabled} label={label} ref={ref}/>
</Container>
);
});
위와 같이 예시 코드가 있다.
해당 코드는 Input 컴포넌트를 사용하는 부모 컴포넌트가 ref를 통해 inputRef.current를 직접 접근할 수 있도록 하는 것이다.
Input는 forwardRef를 통해 ref를 전달받아, 내부적으로 useRef를 사용해 inputRef를 생성하고, <input> 에 연결한다.
useInperativeHandle을 사용하여 부모 컴포넌트가 ref를 통해 inputRef.current를 직접 접근할 수 있도록 허용해주는 것이다.
만약 여기서 useImperativeHandle(ref, () ⇒ inputRef.current); 가 없다면, 예를들어 current 값이 undefined가 되어 focus, value 등 직접 호출할 수 없다.
즉, useImperativeHandle을 사용하지 않으면 ref가 Container에 바인딩되어 input에 조작할 수 없다!
그렇다면 여기서 궁금한건, forwardRef는 useImperativeHandle와 무조건 함께 쓰일까?
그건 아니다!
forwardRef만 사용하고 싶을 때도 있다.
부모 컴포넌트에서 자식 컴포넌트에 ref를 이용하여 직접 연결만하고, DOM 요소에는 직접 접근이 가능하다
const Input = forwardRef<HTMLInputElement, TextFieldProps>(
({ label, ...props }, ref) => {
return <input ref={ref} {...props} />;
}
);
와 같이 사용을 하면, 부모 컴포넌트에서는 focus 함수를
const ParentComponent = () => {
const inputRef = useRef<HTMLInputElement>(null);
return (
<div>
<TextField ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>포커스</button>
</div>
);
};
위와 같이 사용할 수 있다.
그럼 언제 useImperativeHandle을 사용해야할까?
간단히 말하면, 부모에게 전달할 값이나 동작을 커스텀하고 싶을 때 사용한다.
특정 메소드만 노출하거나, ref가 특정 값을 반환하도록 제어할 때 유용하다.
const Input = forwardRef<HTMLInputElement, TextFieldProps>(
({ label, ...props }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
clear: () => {
if (inputRef.current) inputRef.current.value = '';
},
}));
return <StyledInput ref={inputRef} {...props} />;
}
);
위와 같이 사용을 하면 부모 컴포넌트에서는 focus와 clear 메소드만 사용이 가능하다.
즉, 특정 메소드만 노출하거나, 동작을 제한하고 싶을때 useImperativeHandle을 사용한다.
그럼 forwardRef만 사용해도되는게 아닌가?
사실 나도 이렇게 생각하였다.
useImperativeHandle 훅을 왜 사용하지?라는 의문점을 가져, 이번에 공부를 하게 된 것이다.
useImperativeHandle이 필요한 경우에 대해 알아보자
- 불필요한 내부 값 노출 방지(캡슐화)
- ref가 input의 DOM 요소를 직접 가리키면, 부모에서 inputRef.current.value = … 과 같은 방식으로 내부 상태를 강제로 조작할 수 있다.
- 이렇게 되면, 컴포넌트 내부에서 관리해야하는 값들이 있을 때 useImperativeHandle을 이용하여 직접 노출되지 않도록 값을 제한한다!
- 다중 Ref 관리
- 컴포넌트 내부에 여러 개의 ref가 있을 때, 부모가 특정 요소만 컨트롤하도록 만들고 싶을 수도 있다.
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(ref, () => ({
focusInput: () => inputRef.current?.focus(),
clickButton: () => buttonRef.current?.click(),
}));
return (
<div>
<input ref={inputRef} {...props} />
<button ref={buttonRef}>버튼</button>
</div>
);
});
const Parent = () => {
const inputRef = useRef<{ focusInput: () => void; clickButton: () => void }>(null);
return (
<div>
<CustomInput ref={inputRef} />
<button onClick={() => inputRef.current?.focusInput()}>입력창 포커스</button>
<button onClick={() => inputRef.current?.clickButton()}>버튼 클릭</button>
</div>
);
};
위와 같이 같은 ref를 이용하여 특정 요소를 컨트롤하게 이용하였다.
- 외부 라이브러리 또는 커스텀 훅과 연동할 때
- 외부 라이브러리 경우 특정 DOM을 다뤄야하는데, 부모 컴포넌트가 직접 ref로 다루면 내부 구현이 바뀌었을 때 문제가 생길 수 있으므로, 필요한 부분만 노출시켜 유지보수 용이하게 만들자!