리액트 19에서는 다양한 업데이트가 있었습니다. 당연히 이걸 잘 알아야 리액트를 잘 쓸 수 있는거겠죠? 이번업데이트에서 ref 관련해서는 정말 큰 변화들이 있었어요!
오늘은 React 19에서 달라진 ref의 핵심 변화와 많은 분들이 놓치고 있는 흔한 오해들에 대해 깊이 있게 알아보겠습니다.
React 19 이전에는 함수 컴포넌트에 ref를 전달하려면 forwardRef
가 필수였어요:
// React 18 이전 방식
const Input = forwardRef<HTMLInputElement, { placeholder: string }>(
({ placeholder }, ref) => {
return <input ref={ref} placeholder={placeholder} />;
}
);
React 19에서는 ref를 일반 prop처럼 사용할 수 있어요:
// React 19 방식 - 훨씬 간단!
function Input({
placeholder,
ref,
}: {
placeholder: string;
ref?: React.Ref<HTMLInputElement>;
}) {
return <input ref={ref} placeholder={placeholder} />;
}
forwardRef의 보일러플레이트가 완전히 사라졌죠!
React 19에서 ref 콜백에서 cleanup 함수를 반환할 수 있게 되었어요. 이 기능은 React 19에서 정식으로 등장했습니다!
const handleRef = (node: HTMLDivElement | null) => {
if (!node) return;
console.log("요소 마운트!");
const handleClick = () => console.log("클릭!");
node.addEventListener("click", handleClick);
// cleanup 함수 반환 - 돔이 사라질 때 실행되어요!
return () => {
console.log("cleanup 실행!");
node.removeEventListener("click", handleClick);
};
};
많은 분들이 useRef
를 DOM 참조 전용으로 생각하시는데, 사실 그냥 값 저장하는 훅이에요!
// DOM 참조뿐만 아니라
const domRef = useRef<HTMLDivElement>(null);
// 어떤 값이든 저장 가능
const timerRef = useRef<NodeJS.Timeout | null>(null);
const countRef = useRef(0);
const objectRef = useRef({ name: "React", version: 19 });
더 놀라운 건, useCallback과 useMemo도 useRef로 구현할 수 있어요! (실제 구현체는 다르지만요)
// useCallback을 useRef로 구현
function useMyCallback<T extends (...args: any[]) => any>(
callback: T,
deps: React.DependencyList
): T {
const ref = useRef<{ callback: T; deps: React.DependencyList }>({
callback,
deps,
});
if (!shallowEqual(deps, ref.current.deps)) {
ref.current = { callback, deps };
}
return ref.current.callback;
}
왜냐하면 결국 모든 훅들은 fiber 노드의 memoizedState에 저장되는 값들일 뿐이거든요!
이것도 틀렸어요! ref의 진짜 스펙은 "DOM 노드를 받는 콜백 함수"입니다:
// useRef 사용
<div ref={myRef} />
// 콜백 함수 직접 사용
<div ref={(node) => console.log('마운트!', node)} />
// 커스텀 함수도 가능
const handleRef = (node: HTMLDivElement | null) => {
// 원하는 로직
};
<div ref={handleRef} />
여기서 중요한 건 ref 콜백이 언제 실행되느냐예요. useEffect와 비교해볼까요?
function TimingExample() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("useEffect 실행");
}, []);
const refCallback = useCallback(() => {
console.log("ref 콜백 실행");
}, []);
return (
<div>
<div ref={refCallback}>카운트: {count}</div>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
⚠️ 주의: ref에 할당되는 함수가 달라지면 리렌더링할 때마다 실행되니 주의해주세요! 그래서 useCallback으로 감싸주는 것이 좋습니다.
실행 순서:
ref 콜백은 DOM의 생명주기와 직접 연결되어 있어서:
이제 이런 지식을 바탕으로 어떤 멋진 것들을 만들 수 있는지 보여드릴게요!
밖에 클릭하면 닫히는 거, 매번 useEffect로 구현하시나요?
// 기존 방식 - 번거로워요 😵
useEffect(() => {
const handleClick = (e) => {
if (ref.current && !ref.current.contains(e.target)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, []);
✨ 이벤트 핸들러는 DOM 가까이에 있어야 자연스럽죠!
const onOutsideClick = useOutsideClick();
<div ref={onOutsideClick(() => setIsOpen(false))}>메뉴 내용</div>;
구현은 이렇게:
import { useEffect, useRef, useCallback } from "react";
type OutsideClickCallback = () => void;
export const useOutsideClick = () => {
// 현재 요소와 콜백을 저장
const elementRef = useRef<HTMLElement | null>(null);
const callbackRef = useRef<OutsideClickCallback>(() => {});
useEffect(() => {
const handleClick = (e: MouseEvent) => {
const element = elementRef.current;
if (!element) return;
if (!element.contains(e.target as Node)) {
callbackRef.current();
}
};
// 모바일도 고려!
document.addEventListener("mousedown", handleClick);
document.addEventListener("touchstart", handleClick, { passive: true });
return () => {
document.removeEventListener("mousedown", handleClick);
document.removeEventListener("touchstart", handleClick);
};
}, []);
// stable한 ref 콜백 - useEvent 패턴 활용
const stableCallback = useCallback((element: HTMLElement | null) => {
elementRef.current = element;
if (!element) return;
// React 19 cleanup 활용!
return () => {
elementRef.current = null;
};
}, []);
// 콜백을 업데이트하고 stable ref를 반환하는 함수
return (callback: OutsideClickCallback) => {
callbackRef.current = callback;
return stableCallback;
};
};
💡 useEvent 패턴: 콜백 함수는 stable하게 유지하되, 최신 값(옵션)에는 접근할 수 있는 패턴이에요. React 팀이 RFC로 제안한 useEvent 훅과 같은 아이디어로, "latest ref pattern"이라고도 불립니다.
핵심 아이디어:
!element.contains(e.target)
이면 콜백 실행DOM이 사라질 때 애니메이션을 부여하려면? 당연히 다시 DOM을 원래 위치에 삽입하고, 사라지는 효과 주고, 다시 제거하면 됩니다!
ref 콜백으로 이걸 통제하는 방법을 보여드릴게요!
import { useRef, useCallback } from "react";
export type FadeOptions = {
duration?: number;
};
export const useFade = () => {
const containerRef = useRef<HTMLElement | null>(null);
const optionsRef = useRef<FadeOptions>({});
// stable한 ref 콜백 - useEvent 패턴 활용
const stableCallback = useCallback((element: HTMLElement | null) => {
const { duration = 300 } = optionsRef.current;
if (!element) return;
// fadeIn 효과
element.style.opacity = "0";
element.style.transition = `opacity ${duration}ms ease-out`;
containerRef.current = element.parentElement;
requestAnimationFrame(() => {
element.style.opacity = "1";
});
// cleanup - 사라질 때 fadeOut!
return () => {
if (!containerRef.current || !element) return;
// 1. 복사본을 원래 위치에 삽입
const clone = element.cloneNode(true) as HTMLElement;
clone.style.opacity = "1";
clone.style.transition = `opacity ${duration}ms ease-out`;
containerRef.current.appendChild(clone);
// 2. fadeOut 효과
requestAnimationFrame(() => {
clone.style.opacity = "0";
});
// 3. 애니메이션 후 제거
setTimeout(() => {
clone.remove();
}, duration);
};
}, []);
// 옵션을 업데이트하고 stable 콜백을 반환하는 함수
return (options: FadeOptions = {}) => {
optionsRef.current = options;
return stableCallback;
};
};
💡 useEvent 패턴: 여기서도 같은 패턴을 사용했어요. ref 콜백은 stable하게 유지하되, 최신 콜백 함수에는 접근할 수 있도록 했습니다. 이는 Kent C. Dodds가 소개한 "latest ref pattern"으로, React 팀이 RFC로 제안한 useEvent 훅과 같은 아이디어입니다.
하나의 요소에 여러 ref를 적용하고 싶다면? mergeRefs 유틸리티로 금방이죠:
export function mergeRefs<T = any>(
...refs: Array<React.Ref<T> | undefined>
): React.RefCallback<T> {
return (element: T | null) => {
const cleanups: Array<(() => void) | undefined> = [];
refs.forEach((ref) => {
if (!ref) return;
if (typeof ref === "function") {
const cleanup = ref(element);
if (typeof cleanup === "function") {
cleanups.push(cleanup);
}
} else if ("current" in ref) {
(ref as React.MutableRefObject<T | null>).current = element;
}
});
// 클린업 함수들이 있으면 합쳐서 반환
if (cleanups.length > 0) {
return () => {
cleanups.forEach((cleanup) => {
if (cleanup) cleanup();
});
};
}
};
}
이런 활용법 뭔가 참신하신가죠? 사실 이건 제 아이디어가 아니라 스벨트를 참고한거에요. (제 닉네임을 확인 ㅎㅎ;;)
Svelte 문법에는 이미 이런걸로 애니메이션이나 outside click을 구현하는 스펙이 있거든요. 타 프레임워크의 인사이트가 React에서도 적용이 되네요!
React 19의 ref 변화는 단순한 문법 개선이 아니라 더 나은 개발 경험을 제공합니다:
여러분도 React 19의 새로운 ref 기능들을 활용해서 더 선언적이고 효율적인 코드를 작성해보세요! 🚀
💼 이력서 서비스 운영합니다!
개발자 이력서 작성에 도움이 필요하시다면: fe-resume.coach/ai-resume