React Fragment를 쓰면 컴포넌트가 여분의 <div>로 감싸지 않고 여러 요소를 반환할 수 있습니다. 첫 번째 요소 앞에 <>를, 마지막 요소 뒤에 </>를 쓰면 React는 페이지에 자기 노드를 만들지 않고 요소들을 부모 안에 바로 넣습니다.
각 Greeting은 제목과 문단을 나란히 반환합니다. <>와 </> 줄을 지우면 미리보기에 빌드 오류가 대신 나타납니다. 함수가 이제 값 두 개를 반환하려고 하기 때문입니다.
컴포넌트에 하나의 루트가 필요한 이유
JSX는 함수 호출을 더 보기 좋게 쓰는 방법입니다. <h2>Hi</h2>는 대략 jsx('h2', { children: 'Hi' })라는 하나의 표현식으로 컴파일됩니다. return 문은 값 하나만 돌려줄 수 있으므로, 아무것도 감싸지 않은 형제 태그 두 개는 return a b라고 쓰는 것과 같습니다. 컴파일러는 "Adjacent JSX elements must be wrapped in an enclosing tag"라며 멈춥니다.
프래그먼트가 생기기 전에는 래퍼 <div>로 해결했습니다. 그래도 동작하지만 div는 실제 요소입니다. DOM에 나타나고, CSS 레이아웃에 참여하며, 어떤 부모는 아예 자식으로 받아들이지 않습니다. 프래그먼트는 하나의 루트 규칙을 만족시킨 뒤 사라집니다.
아무것도 추가하지 않는다는 것을 직접 확인할 수 있습니다. 아래 예제는 같은 두 항목을 프래그먼트와 div로 각각 렌더링한 뒤, 각 컨테이너의 직접적인 자식 수를 셉니다.
콘솔에는 프래그먼트 쪽은 두 span을 뜻하는 2가, div 쪽은 그것들을 감싼 div 하나를 뜻하는 1이 출력됩니다. flex나 grid 컨테이너에서는 이 차이에 따라 span이 각각 항목으로 배치될지, 하나로 묶일지가 결정됩니다.
짧은 문법과 Fragment 컴포넌트
프래그먼트를 쓰는 방법은 두 가지이며, 같은 결과로 컴파일됩니다.
import { Fragment } from 'react';
// Short syntax, no import needed
<>
<dt>React</dt>
<dd>A library for building user interfaces</dd>
</>
// Long form
<Fragment>
<dt>React</dt>
<dd>A library for building user interfaces</dd>
</Fragment>
거의 모든 곳에서 <>...</>를 사용하세요. <Fragment>가 필요한 경우는 하나입니다. 프래그먼트에 key가 필요할 때입니다. 짧은 문법은 속성을 받을 수 없으므로 <key={id}>는 올바른 JSX가 아닙니다. 오래된 코드에서는 <React.Fragment>도 보게 되는데, 기본 React import를 통해 접근하는 같은 컴포넌트입니다.
목록에서 key를 가진 프래그먼트
map으로 목록을 렌더링할 때는 React가 렌더링 사이에 각 항목을 추적할 수 있도록 모든 항목에 key가 필요합니다(이유는 목록과 key 페이지에서 설명합니다). 각 항목이 요소 하나가 아니라 요소들의 묶음이라면 key는 프래그먼트에 붙여야 하므로 긴 형태가 필요합니다.
대표적인 예가 설명 목록입니다. 각 항목은 <dt>와 <dd>이고, <dl>은 이것들을 직접 담아야 합니다.
Reverse를 클릭하면 각 쌍이 자기 key를 가지고 있으므로 쌍 단위로 자리를 바꿉니다. 각 쌍을 감싼 래퍼 <div>도 key를 받을 수 있고 dl 안에서 올바른 HTML이지만, 프래그먼트라면 생기지 않을 세 번째 단계를 마크업에 추가합니다.
테이블 안의 프래그먼트
엉뚱한 래퍼가 가장 큰 문제를 일으키는 곳은 테이블입니다. <tr>은 <td>와 <th> 셀을 직접적인 자식으로 기대합니다. 셀 여러 개를 반환하는 컴포넌트가 그것들을 <div>로 감싸면, 브라우저는 그 div를 이상한 셀 하나로 취급하고 안의 셀들은 헤더와 맞지 않게 됩니다.
프래그먼트 행은 가격이 Price 아래에, 재고가 Stock 아래에 있습니다. div 행에서는 두 셀이 모두 Price 칼럼에 끼어 있고 Stock 칼럼은 비어 있습니다. 개발 환경에서는 React가 <div>는 <tr>의 자식이 될 수 없다는 경고도 출력합니다. 미리보기는 프로덕션 빌드처럼 실행되어 그 검사를 건너뛰므로, 여기서는 깨진 레이아웃만 보입니다. 서버 렌더링에서는 브라우저의 HTML 파서가 잘못 놓인 div를 옮기기 때문에 같은 실수가 하이드레이션 오류로 이어질 수도 있습니다.
같은 규칙이 <li> 요소를 담아야 하는 <ul>과 <ol>, <option> 요소를 담아야 하는 <select>에도 적용됩니다. 컴포넌트가 이런 부모 안에 직접 들어가야 하는 조각들을 반환할 때는 프래그먼트를 반환하세요.
프래그먼트와 배열
컴포넌트는 요소의 배열을 반환할 수도 있고, React는 프래그먼트처럼 순서대로 렌더링합니다. React는 반환된 배열을 목록으로 취급하므로, map의 결과와 마찬가지로 배열의 각 요소에 자기 key가 필요합니다.
function Cells() {
return [<td key="price">$4</td>, <td key="stock">In stock</td>];
}
동작은 하지만 더 지저분합니다. 요소 사이의 쉼표, 각 요소의 key, 마크업이 커지면 놓치기 쉬운 대괄호가 생깁니다. 고정된 요소 묶음에는 프래그먼트를 쓰고, 배열은 본래 용도인 데이터를 map한 결과에 남겨 두세요.
프래그먼트가 할 수 없는 것
프래그먼트는 요소를 렌더링하지 않으므로 클래스, 스타일, 이벤트 핸들러, ref를 붙일 곳이 없습니다. <Fragment className="row">는 아무 쓸모가 없고, React가 프래그먼트에서 읽는 prop은 key뿐입니다. 묶음에 스타일을 주거나 클릭을 감지해야 한다면 결국 실제 요소가 필요하며, 그래도 괜찮습니다. 그 자리에서 올바른 요소를 고르세요. 행을 묶는 <tbody>나 목록 항목을 담는 <li>처럼요.
프래그먼트는 조건부 렌더링 안을 포함해 요소가 올 수 있는 곳이면 어디서든 동작합니다. {show && <>...</>}는 묶음 전체를 한 번에 보여 주거나 숨깁니다.
자주 묻는 질문
React에서 Fragment란 무엇인가요?
Fragment는 DOM 노드를 추가하지 않고 컴포넌트가 여러 요소를 하나의 값으로 반환하게 해 주는 래퍼입니다. <><h1 /><p /></>는 h1과 p를 부모 안에 직접 넣습니다.
<>와 <Fragment>의 차이는 무엇인가요?
결과는 같습니다. <>...</>는 짧은 문법입니다. <Fragment>는 react에서 import하는 긴 형태이며, 목록에서 프래그먼트를 렌더링할 때 필요한 key를 받을 수 있는 유일한 형태입니다.
div 대신 Fragment를 쓰는 이유는 무엇인가요?
div는 실제 요소입니다. 직접적인 자식을 기대하는 레이아웃(flex와 grid 컨테이너, ul, table, tr)을 깨뜨릴 수 있고 DOM을 어지럽힙니다. Fragment는 아무것도 추가하지 않습니다.
Fragment에 className이나 onClick을 줄 수 있나요?
아니요. Fragment는 요소를 렌더링하지 않으므로 클래스, 스타일, 이벤트 핸들러를 붙일 곳이 없습니다. 받을 수 있는 prop은 key뿐입니다. 그런 것이 필요하면 실제 요소를 사용하세요.
"Adjacent JSX elements must be wrapped in an enclosing tag" 오류는 왜 나나요?
컴포넌트가 아무것으로도 감싸지 않은 형제 요소 두 개를 반환했기 때문입니다. JSX는 하나의 표현식으로 컴파일되므로 형제 요소들을 <>...</>로 감싸세요.