ref는 useRef를 통해 반환받은 객체로, 리액트 컴포넌트의 특수한 props인 ref에 넣어 HTMLElement에 접근하는 용도로 자주 사용된다.
이러한 ref를 상위 컴포넌트에서 하위 컴포넌트로 전달하기 위해서는 어떻게 해야할까?
React 18이하의 버전들은 ref를 단순히 props로 넘겨주게되면 리액트에서 ref는 props로 쓸 수 없다는 경고문과 함께 접은을 시도할 경우 undefined를 반환한다.
따라서 이러한 경우 ref를 하위 컴포넌트에 넘겨주기 위해 나온 hook이 바로 forwardRef이다.
하지만 예약어로 지정된 ref가 아닌 다른 props의 이름으로 넘겨줄시 정상적으로 작동한다.
그렇다면 forwardRef가 존재하는 이유는 ref를 전달하는 데 있어서 일관성을 제공하기 위해 탄생하였고, 완전한 네이밍의 자유가 주어진 props보다는 forwardRef를 사용하면 더 확실하게 ref를 전달할 것임을 예측할 수 있다.
forwardRef는 다음과 같이 사용한다.
const ChildComponent = forwardRef<RefType, propsType>((props,ref)=>{
return <div ref={ref}><div/>
}
ref를 받고자 하는 컴포넌트를 forwardRef로 감싸고, 두 번째 인수로 ref를 전달받아 사용한다.
React 18버전까지는 위와같이 forwardRef를 사용하여 ref를 전달하였지만, 새로운 버전인 React 19부터는 forwardRef는 사용되지 않는다.
물론 이전버전과의 호환성을위해 사용해도 정상적으로 작동하나, 굳이 사용할 필요가 없어졌다.
다른 props와 마찬가지로 props객체를 통해 ref를 넘겨주어 바로 사용하면 된다.
interface PropsType {
ref: Ref<HTMLDialogElement>;
}
const ResultModal = ({ ref }: PropsType) => {
return <div ref={ref}><div/>
}
useImperativeHandle은 부모에게서 넘겨받은 ref를 원하는 대로 수정할 수 있는 hook이다.
이를 사용하면 부모 컴포넌트에서 노출되는 값을 원하는 대로 바꿀 수 있다.
기본 형식은 useImperativeHandle(ref, createHandle, dependencies?)으로 다음과 같이 사용한다.
import { Ref, useImperativeHandle } from 'react';
interface MyInputProps{
ref: Ref<>;
}
function MyInput({ ref }: MyInputProps) {
useImperativeHandle(ref, () => {
return {
// ... 메서드를 여기에 입력하세요 ...
};
}, []);
// ...
그렇다면 이러한 hook은 어떤 상황에 사용할까?
custom Modal을 제작한다고 해보자
import { Ref } from "react";
interface ResultModalProps {
ref: Ref<HTMLDialogElement>;
result: string;
targetTime: number;
}
const ResultModal = ({ result, targetTime, ref }: ResultModalProps) => {
return (
<dialog
ref={ref}
className="border-none rounded-lg p-8 bg-[#d7fcf8] backdrop:bg-black backdrop:opacity-90"
>
<h2 className="font-[Handjet, monospace] m-0 mb-1 text-5xl uppercase">You {result}</h2>
<p className="mx-0 my-2 text-xl">
The target time was <strong className="text-[#10655b]">{targetTime} seconds.</strong>
</p>
<p className="mx-0 my-2 text-xl">
You stopped the timer with <strong className="text-[#10655b]">X seconds left.</strong>
</p>
<form method="dialog" className="text-right">
<button className="mt-4 py-2 px-4 border-none rounded bg-[#12352f] text-[#edfcfa] text-xl cursor-pointer hover:bg-[#051715]">
Close
</button>
</form>
</dialog>
);
};
export default ResultModal;
import { useRef, useState } from "react";
import ResultModal from "./ResultModal";
interface TimerChallengeProps {
title: string;
targetTime: number;
}
const TimerChallenge = ({ title, targetTime }: TimerChallengeProps) => {
const timer = useRef<number>(null);
const dialog = useRef<HTMLDialogElement>(null);
const [timerStarted, setTimerStarted] = useState(false);
const handleStart = () => {
timer.current = setTimeout(() => {
dialog.current?.showModal();
}, targetTime * 1000);
setTimerStarted(true);
};
const handleStop = () => {
if (timer.current) clearTimeout(timer.current);
setTimerStarted(false);
};
return (
<>
<ResultModal ref={dialog} result="lost" targetTime={targetTime} />
<section
className="flex flex-col items-center justify-center p-8 mx-auto my-8 rounded-md shadow-md w-80 text-[#221c18]"
style={{ background: "linear-gradient(#4df8df, #4df0f8)" }}
>
<h2 className="text-2xl tracking-widest m-0 text-center uppercase text-[#221c18]">
{title}
</h2>
<p className="border border-solid border-[#46cebe] rounded py-1 px-2 m-2">
{targetTime} second{targetTime > 1 ? "s" : ""}
</p>
<p>
<button
className="mt-4 py-2 px-4 border-none rounded bg-[#12352f] text-[#edfcfa] text-xl cursor-pointer hover:bg-[#051715]"
onClick={timerStarted ? handleStop : handleStart}
>
{timerStarted ? "Stop" : "Start"} Challenge
</button>
</p>
<p style={timerStarted ? { animation: "flash 1s infinite" } : {}}>
{timerStarted ? "Time is running..." : "Timer inactive"}
</p>
</section>
</>
);
};
export default TimerChallenge;
위의 두 코드를 통해 TimerChallenge컴포넌트에서 useRef로 ResultModal을 연결하여 사용하고 있다.
handleStart함수에서 dialog.current?.showModal()을 통해 ResultModal을 show상태로 바꿔주는데, 이때 showModal은 <dialog>의 속성중 하나이다.
물론 여기서는 showModal이라는 직관적인 이름을 사용하고 있지만, 그렇지 않은 경우 useImperativeHandle을 사용하여 추가적인 동작을 정의할 수 있다.
import { Ref, useImperativeHandle, useRef } from "react";
interface ResultModalProps {
ref: Ref<HandleDialog>;
result: string;
targetTime: number;
}
export interface HandleDialog {
open: () => void;
}
const ResultModal = ({ result, targetTime, ref }: ResultModalProps) => {
const dialog = useRef<HTMLDialogElement>(null);
useImperativeHandle(ref, () => {
return {
open() {
dialog.current?.showModal();
}
};
});
return (
<dialog
ref={dialog}
className="border-none rounded-lg p-8 bg-[#d7fcf8] backdrop:bg-black backdrop:opacity-90"
>
<h2 className="font-[Handjet, monospace] m-0 mb-1 text-5xl uppercase">You {result}</h2>
<p className="mx-0 my-2 text-xl">
The target time was <strong className="text-[#10655b]">{targetTime} seconds.</strong>
</p>
<p className="mx-0 my-2 text-xl">
You stopped the timer with <strong className="text-[#10655b]">X seconds left.</strong>
</p>
<form method="dialog" className="text-right">
<button className="mt-4 py-2 px-4 border-none rounded bg-[#12352f] text-[#edfcfa] text-xl cursor-pointer hover:bg-[#051715]">
Close
</button>
</form>
</dialog>
);
};
export default ResultModal;
import { useRef, useState } from "react";
import ResultModal, { HandleDialog } from "./ResultModal";
interface TimerChallengeProps {
title: string;
targetTime: number;
}
const TimerChallenge = ({ title, targetTime }: TimerChallengeProps) => {
const timer = useRef<number>(null);
const dialog = useRef<HandleDialog>(null);
const [timerStarted, setTimerStarted] = useState(false);
const handleStart = () => {
timer.current = setTimeout(() => {
dialog.current?.open();
}, targetTime * 1000);
setTimerStarted(true);
};
const handleStop = () => {
if (timer.current) clearTimeout(timer.current);
setTimerStarted(false);
};
return (
<>
<ResultModal ref={dialog} result="lost" targetTime={targetTime} />
<section
className="flex flex-col items-center justify-center p-8 mx-auto my-8 rounded-md shadow-md w-80 text-[#221c18]"
style={{ background: "linear-gradient(#4df8df, #4df0f8)" }}
>
<h2 className="text-2xl tracking-widest m-0 text-center uppercase text-[#221c18]">
{title}
</h2>
<p className="border border-solid border-[#46cebe] rounded py-1 px-2 m-2">
{targetTime} second{targetTime > 1 ? "s" : ""}
</p>
<p>
<button
className="mt-4 py-2 px-4 border-none rounded bg-[#12352f] text-[#edfcfa] text-xl cursor-pointer hover:bg-[#051715]"
onClick={timerStarted ? handleStop : handleStart}
>
{timerStarted ? "Stop" : "Start"} Challenge
</button>
</p>
<p style={timerStarted ? { animation: "flash 1s infinite" } : {}}>
{timerStarted ? "Time is running..." : "Timer inactive"}
</p>
</section>
</>
);
};
export default TimerChallenge;
위의 코드를 통해 TimerChallenge에서 ResultModal을 열기위한 ref의 속성인 showModal을 open이라는 직관적인 이름으로 바꾸어줄 수 있다.
상위 컴포넌트로부터 전달받은 ref를 useImperativeHandle의 props으로 넣어주고, 하위 컴포넌트에서 새로운 ref를 정의하여 해당 ref의 속성을 새로 정의하여 <dialog>의 ref로 전달해주었다.