React 19.3 Fragment Refs 사용법의 요점은 <Fragment ref={ref}>로 렌더링하면 React가 ref 값으로 FragmentInstance를 넣어 준다는 것이다. 이 인스턴스는 Fragment가 감싼 DOM 노드를 래퍼 div를 추가하지 않고 하나의 그룹처럼 다룬다. 여러 형제 요소에 클릭 리스너를 한 번에 붙이거나, 깊이 중첩된 입력 필드로 포커스를 옮기거나, IntersectionObserver로 묶음 전체를 관찰할 때 쓰는 DOM 환경용 기능이다.
이 글은 2026-10 기준 react.dev 블로그와 Fragment 레퍼런스에 근거해 준비 → 구현 → 결과 확인 → 주의사항 순서로 정리한다.
목차
React 19.3 Fragment Refs 적용 대상과 버전
React 19.3 릴리스 공지는 앞서 실험적 API로 공개했던 View Transitions와 Fragment Refs가 React 19.3에서 stable이 되었다고 밝혔다. 원문 문장은 “We’re excited to announce that both of these are now stable in React 19.3!”이다.
최신 버전은 React 버전 페이지에 따르면 19.3(2026년 9월 9일)이며, 블로그 글과 v19.3.0 CHANGELOG 링크도 함께 제공된다. 따라서 이 글의 코드는 React 19.3 이상을 전제로 한다.
공지가 정의하는 Fragment Ref의 역할은 “Fragment의 DOM을 구조 변경 없이 그룹으로 다루는 FragmentInstance 제공”이다. 대상은 Fragment 아래에 렌더링된 DOM 노드이고, 제공되는 메서드도 addEventListener, focus, observeUsing, getClientRects, scrollIntoView처럼 브라우저 DOM API와 짝을 이룬다.
React Native, 서버 컴포넌트, SSR 환경에서의 Fragment ref 동작은 19.3 블로그와 Fragment 레퍼런스 어디에도 설명이 없다. 이 글은 브라우저 DOM 환경만 다룬다.
시작 전 준비 사항
준비할 항목은 세 가지다.
- React 19.3 사용: Fragment Refs가 stable로 제공되는 버전이다. 설치·업그레이드 명령은 19.3 블로그와 버전 페이지 모두 명시하지 않으므로, 사용 중인 패키지 매니저로
react와react-dom버전을 19.3으로 맞춘다. Fragment명시적 import: 단축 문법<>...</>에는 ref를 붙일 수 없다.- ref 값 준비:
useRef로 만든 ref 객체나 ref 콜백 함수 중 하나를 쓴다.
두 번째 항목은 Fragment 레퍼런스의 Caveats에 그대로 적혀 있다. 기본 형태는 다음과 같다.
<Fragment ref={yourRef}>...</Fragment>
`<>…>`로 작성된 Fragment에 ref를 전달하는 방법은 없다. ref가 필요한 지점만 `import { Fragment } from ‘react’` 후 `
src/
├── components/
│ ├── ClickableFragment.jsx ← 이벤트 리스너 그룹
│ ├── FormFields.jsx ← 포커스 그룹
│ └── ObservedGroup.jsx ← IntersectionObserver 연결
└── App.jsx
Fragment ref 구현 절차
블로그 공지의 최소 예제는 useRef로 ref를 만들고, useEffect에서 fragmentRef.current로 인스턴스를 꺼내 focus()를 호출한다.
function Component() {
const fragmentRef = useRef(null);
useEffect(() => {
const fragmentInstance = fragmentRef.current;
fragmentInstance.focus();
}, []);
return (
<Fragment ref={fragmentRef}>
{posts.map(post => (
<Heading key={post.id}>
{post.title}
</Heading>
))}
</Fragment>
)
}
모든 기능은 이 패턴의 변형이다. ref를 만들고, Fragment에 연결하고, 커밋 이후(Effect 또는 이벤트 핸들러)에 FragmentInstance 메서드를 호출한다.
1단계: 래퍼 없이 이벤트 리스너 붙이기

레퍼런스의 ‘Adding event listeners without a wrapper element’ 예제다. useEffect 안에서 인스턴스가 null인지 먼저 확인하고, addEventListener로 등록한 뒤 cleanup에서 removeEventListener로 해제한다. 의존성 배열은 [onClick]이다.
import { Fragment, useState, useRef, useEffect } from 'react';
function ClickableFragment({ children, onClick }) {
const fragmentRef = useRef(null);
useEffect(() => {
const fragmentInstance = fragmentRef.current;
if (fragmentInstance === null) {
return;
}
fragmentInstance.addEventListener('click', onClick);
return () => {
fragmentInstance.removeEventListener(
'click',
onClick
);
};
}, [onClick])
return (
<Fragment ref={fragmentRef}>
{children}
</Fragment>
);
}
addEventListener는 Fragment의 모든 first-level DOM 자식에 리스너를 추가하고 undefined를 반환한다. 사용하는 쪽은 버튼 세 개를 감싸기만 하면 된다.
export default function App() {
const [clicks, setClicks] = useState(0);
return (
<>
<p>Total clicks: {clicks}</p>
<ClickableFragment onClick={() => setClicks(c => c + 1)}>
<button>Button A</button>
<button>Button B</button>
<button>Button C</button>
</ClickableFragment>
</>
);
}
같은 인스턴스에는 dispatchEvent(event)도 있다. Fragment에 이벤트를 디스패치하며, bubbles가 true면 Fragment의 부모 DOM으로 버블링된다. 반환값은 취소되지 않았을 때 true, preventDefault()가 호출됐을 때 false다.
fragmentRef.current.dispatchEvent(new Event('custom', { bubbles: true }));
2단계: 그룹 단위 포커스 관리
‘Managing focus across a group of elements’ 예제는 버튼 클릭 시 focus(), focusLast(), blur()를 호출한다. 이벤트 핸들러에서 부르므로 Effect가 필요 없다.
import { Fragment, useRef } from 'react';
function FormFields({ children }) {
const fragmentRef = useRef(null);
return (
<>
<div className="buttons">
<button onClick={() => {
fragmentRef.current.focus();
}}>
Focus first
</button>
<button onClick={() => {
fragmentRef.current.focusLast();
}}>
Focus last
</button>
<button onClick={() => {
fragmentRef.current.blur();
}}>
Blur
</button>
</div>
<Fragment ref={fragmentRef}>
{children}
</Fragment>
</>
);
}
focus(options?)는 중첩 자식을 깊이 우선으로 탐색해 첫 번째 포커스 가능 노드에 포커스를 준다. focusLast(options?)는 마지막 포커스 가능 노드가 대상이다. 예제 주석 원문은 “Even though the inputs are deeply nested, focus() searches depth-first to find them.”이다. 입력 필드를 fieldset과 label 안에 넣어 사용하면 이 동작을 확인할 수 있다.
export default function App() {
return (
<FormFields>
<fieldset>
<label>Street <input name="street" /></label>
</fieldset>
<fieldset>
<label>City <input name="city" /></label>
</fieldset>
</FormFields>
);
}
3단계: Observer 연결 Hook 패턴
observeUsing(observer)는 IntersectionObserver 또는 ResizeObserver로 first-level DOM 자식 관찰을 시작한다. 중지할 때는 같은 observer 인스턴스를 unobserveUsing(observer)에 넘긴다. 레퍼런스의 호출 형태는 다음과 같다.
const observer = new IntersectionObserver(callback, options);
fragmentRef.current.observeUsing(observer);
fragmentRef.current.unobserveUsing(observer);
이 호출을 이벤트 예제와 같은 Effect 구조로 감싸면 재사용 가능한 컴포넌트가 된다. 연결과 해제가 같은 Effect 안에 있으므로 언마운트 시 관찰이 정리된다.
import { Fragment, useRef, useEffect } from 'react';
function ObservedGroup({ children, callback, options }) {
const fragmentRef = useRef(null);
useEffect(() => {
const fragmentInstance = fragmentRef.current;
if (fragmentInstance === null) {
return;
}
const observer = new IntersectionObserver(callback, options);
fragmentInstance.observeUsing(observer);
return () => {
fragmentInstance.unobserveUsing(observer);
};
}, [callback, options]);
return <Fragment ref={fragmentRef}>{children}</Fragment>;
}
`callback`이나 `options`가 렌더링마다 새로 만들어지면 Effect가 매번 다시 실행되어 observer도 재생성된다. 상위 컴포넌트에서 값을 고정해 넘기는 편이 안전하다.
scrollIntoView(alignToTop?)는 인자가 true이거나 생략되면 첫 자식을 상단에, false면 마지막 자식을 하단에 정렬한다.
fragmentRef.current.scrollIntoView();
fragmentRef.current.scrollIntoView(false);
측정용 메서드는 세 가지다. getClientRects()는 first-level DOM 자식들의 Array<DOMRect>를 반환한다. getRootNode(options?)의 반환값은 Document, ShadowRoot, 또는 부모 DOM 노드가 없을 때 FragmentInstance 자신이다. compareDocumentPosition(otherNode)는 위치 bitmask를 돌려준다.
const rects = fragmentRef.current.getClientRects();
const root = fragmentRef.current.getRootNode();
const position = fragmentRef.current.compareDocumentPosition(otherElement);
결과 확인 방법
구현이 맞는지는 공식 예제의 기대 동작과 비교해 판단한다.
이벤트 리스너: ClickableFragment로 감싼 Button A, B, C 중 어느 것을 클릭해도 ‘Total clicks’ 숫자가 1씩 올라가야 한다. 특정 버튼에서만 증가하지 않는다면 그 버튼이 Fragment의 first-level 자식이 아니라 다른 요소 안에 들어가 있는지 확인한다.
포커스: ‘Focus first’를 누르면 fieldset > label 안에 중첩된 street 입력 필드로 커서가 이동해야 한다. ‘Focus last’는 city 입력 필드가 대상이다. ‘Blur’를 누르면 Fragment 안에 있던 포커스가 해제된다.
Observer: IntersectionObserver 콜백이 Fragment의 각 first-level DOM 자식에 대해 호출되는지 브라우저 개발자 도구 콘솔에서 확인한다. 텍스트만 있는 Fragment라면 개발 모드 경고가 출력되는 것이 정상이다.
FragmentInstance 주의사항
Fragment 레퍼런스의 Caveats 중 실제 버그로 이어지기 쉬운 항목을 모았다.
대상 범위는 first-level DOM 자식
addEventListener, observeUsing, getClientRects처럼 자식을 대상으로 하는 메서드는 Fragment의 first-level host(DOM) 자식에만 작동한다. 다른 DOM 요소 안에 중첩된 자식은 직접 대상이 되지 않는다. 예외는 focus와 focusLast로, 이 둘은 중첩 자식까지 깊이 우선 탐색한다.
flowchart TD
F[Fragment ref] --> A[first-level DOM 자식]
A --> N[중첩된 자식]
F -.addEventListener / observeUsing / getClientRects.-> A
F -.focus / focusLast 깊이 우선 탐색.-> N
ref가 있는 Fragment의 각 first-level DOM 자식에는 reactFragments 속성(Set<FragmentInstance>)이 추가된다. DOM 노드 쪽에서 자신을 감싼 Fragment 인스턴스를 찾을 때 이 속성을 참조한다.
텍스트 노드와 Activity
observeUsing은 텍스트 노드에서 작동하지 않는다. Fragment에 텍스트 자식만 있으면 개발 모드에서 경고가 출력된다.
addEventListener로 추가한 리스너는 숨겨진 <Activity> 트리에는 적용되지 않는다. 트리가 hidden에서 visible로 바뀌면 자동으로 적용되므로 별도로 다시 등록하지 않아도 된다.
scrollIntoView와 compareDocumentPosition
Fragment의 `scrollIntoView`는 `Element.scrollIntoView()`와 달리 ScrollIntoViewOptions 객체를 받지 않으며, 객체를 넘기면 오류가 발생한다. DOM 요소용 코드를 옮겨 올 때 `{ behavior: ‘smooth’ }` 같은 인자를 그대로 두지 말고 `alignToTop` boolean으로 바꿔야 한다.
blur()는 document.activeElement가 Fragment 안에 있을 때만 동작한다. 포커스가 Fragment 밖에 있으면 호출해도 포커스는 그대로 유지된다.
관련 글
- GPT-6 Sol Luna Astra 모델 비교 — 스펙·요금·React 적용 전략 – GPT-6 라인업은 Astra, Sol, Luna 세 모델로 분화되어 입력 요금 기준 최대 100배 차이가 난다. 세 모델의 포지션, 비용…
- Claude Code 아티팩트 사용법: 게시·재게시 전 보안 점검 체크리스트 – Claude Code 아티팩트는 세션 결과를 claude.ai 페이지로 게시하는 기능이다. v2.1.275~v2.1.284 릴리스 노트에서…
자주 묻는 질문
<>...</> 단축 문법에 ref를 붙일 수 있나?
붙일 수 없다. 'react'에서 Fragment를 import해 <Fragment ref={yourRef}>...</Fragment>로 렌더링해야 한다.
ref 콜백 함수도 사용할 수 있나?
가능하다. ref prop은 선택 항목이며 useRef 등으로 만든 ref 객체나 ref 콜백 함수를 받는다. 어느 쪽이든 React가 넘겨주는 값은 FragmentInstance다.
중첩된 요소의 클릭도 감지되나?
리스너는 first-level DOM 자식에 등록된다. 중첩된 요소는 직접 등록 대상이 아니지만, 그 요소가 first-level 자식 내부에 있다면 해당 자식에 등록된 리스너의 대상 범위 안에 놓인다. 등록 대상을 정확히 통제하려면 Fragment 바로 아래 구조를 기준으로 설계한다.
한국어 공식 문서에서 메서드 설명을 볼 수 있나?
한국어 문서(ko.react.dev)에는 ref prop과 주의사항만 번역되어 있다. FragmentInstance 메서드와 사용 예제는 영문 레퍼런스에서 확인한다.
이어서 볼 주제
Fragment Refs의 기본 흐름은 ref 생성 → <Fragment ref> 연결 → 커밋 이후 FragmentInstance 메서드 호출이고, 실수는 대부분 first-level 자식 범위와 scrollIntoView 인자 형식에서 생긴다. 같은 19.3 릴리스에서 stable이 된 React 19.3 View Transitions는 화면 전환 애니메이션 쪽 기능이라 Fragment ref와 함께 React 19.3 새 기능의 다른 축을 이룬다. React observeUsing IntersectionObserver를 활용한 무한 스크롤이나 노출 추적, 그리고 <Activity>로 숨긴 트리와 이벤트 리스너의 상호작용도 Fragment ref를 실무에 적용할 때 함께 살펴볼 영역이다.