클론 코딩을 하며 공부를 하던 중,
map()
을 사용하여 영화 목록을 보여주는 과정에서React.Fragment
를 사용하는 것을 보았다.
필자였다면 최상위 엘리먼트로<div>
를 쓰고 넘어갔을 것 같은데...
React.Fragment
는 왜 사용하는걸까? 이유와 예시를 알아보도록 하자.
A common pattern in React is for a component to return multiple elements. Fragments let you group a list of children without adding extra nodes to the DOM.
- React 공식 docs -
컴포넌트가 여러 개의 엘리먼트를 반환하는 것은 리액트에서 흔한 패턴이다. Fragments는 당신이 DOM에 추가적인 노드를 추가하지 않고도 자식 요소들을 그룹화 할 수 있게 해준다.
- React 공식 docs -
공식 문서에서는 React.Fragment
에 대하여 위와 같은 멘트와 함께 설명을 시작한다.
여러 자식 요소들을 그룹화하기 쉽다는 말이 애매하게 보인다.
예시를 살펴보자.
아래와 같은 컴포넌트를 반복 생성해야하는 상황이다.
// ItrateComponent.jsx
function ItrateComponent(props) {
return (
<div>
<h2>{props.num}번 반복 컴포넌트입니다.</h2>
<p>반복은 너무 귀찮다.</p>
</div>
);
}
export default ItrateComponent;
// App.js
import ItrateComponent from "./IterateComponent";
export default function App() {
const arr = [1, 2, 3, 4, 5];
return (
<div>
{arr.map((num, index) => (
<ItrateComponent num={num} />
))}
</div>
);
}
생성된 화면은 다음과 같을 것이다.
개발자 도구를 통해 HTML 태그 상태를 살펴보자.
App.js
를 기준으로 보면
<div>
// 여기부터 ItrateComponent.jsx
<div>
<h2></h2>
<p></p>
</div>
<div>
<h2></h2>
<p></p>
</div>
// ... //
</div>
여기서 주목할 것은 다름아닌, ItrateComponent.jsx
에서의 div
태그이다.
div
태그는 아무 의미가 없는 태그이기 때문에, 구획을 나눌 때 대충 쓰기 좋은 태그이다.
단, 의미가 없기 때문에 불필요하다.
문제는...React에서는 최상위 태그 아래에 자식 요소를 넣어줘야지만 컴포넌트를 사용할 수 있다.
즉,
// ItrateComponent.jsx
function ItrateComponent(props) {
return (
// 아래 두 태그를 감싸던 최상위 div 태그가 사라졌다.
<h2>{props.num}번 반복 컴포넌트입니다.</h2>
<p>반복은 너무 귀찮다.</p>
);
}
export default ItrateComponent;
이렇게 사용하면 에러가 발생한다는 것이다.
우리는 반드시 최상위 태그를 작성해야한다.
자, 여기서 공식 문서의 말을 한번 떠올려보자.
Fragments는 당신이 DOM에 추가적인 노드를 추가하지 않고도 자식 요소들을 그룹화 할 수 있게 해준다.
오호...이제 이 말을 이해할 수 있겠다.
위 예시에서 사용한 불필요한 div
태그를 없앨 수 있다는 뜻이구나!
앞서 우리는 Fragment
가 불필요한 태그 생성을 없앨 수 있게 해준다는 것을 알았다.
ItrateComponent.jsx
의 코드를 아래와 같이 수정해보겠다.
import React from "react";
function ItrateComponent(props) {
return (
<React.Fragment>
<h2>{props.num}번 반복 컴포넌트입니다.</h2>
<p>반복은 너무 귀찮다.</p>
</React.Fragment>
);
}
export default ItrateComponent;
import { Fragment } from "react";
function ItrateComponent(props) {
return (
<Fragment>
<h2>{props.num}번 반복 컴포넌트입니다.</h2>
<p>반복은 너무 귀찮다.</p>
</Fragment>
);
}
export default ItrateComponent;
function ItrateComponent(props) {
return (
// 단축된 React.Fragment 사용법
<>
<h2>{props.num}번 반복 컴포넌트입니다.</h2>
<p>반복은 너무 귀찮다.</p>
</>
);
}
export default ItrateComponent;
위 세 가지 예시는 모두 같은 코드이다.
문법만 조금 달라질 뿐이다.
이렇게 사용해도 페이지 렌더링 결과는 같다.
개발자 도구를 사용해서 HTML 태그쪽을 살펴보자.
div
태그를 사용했던 때와 비교했을 때 어떤 것이 달라졌을까?
...이미 질문 속에 정답이 있다.
div
태그가 사라졌다!!
그런데 렌더링된 화면에는 전혀 변화가 없다. 그냥 아까랑 똑같다.
즉, 최상위 태그를 만들기 위해 div
를 사용하지 않고도, 불필요하게 노드를 추가하지 않고도,
React.Fragment
를 사용하면 최상위 노드(태그)를 만들어주지 않아도 컴포넌트를 사용할 수 있다!
공식 문서에는 이를 더 명확하게 보여주는 예시가 있다.
class Columns extends React.Component {
render() {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
);
}
}
class Table extends React.Component {
render() {
return (
<table>
<tr>
<Columns />
</tr>
</table>
);
}
}
table
태그 내에서 Columns
컴포넌트를 사용하는데 문제는..
컴포넌트를 사용하기 위해서는 최상위 노드로 무조건 감싸줘야한다는 것이다.
그래서 Columns
컴포넌트에 불필요하게 div
를 사용해야만 한다.
그래서 HTML 엘리먼트의 구조는 아래와 같아진다.
<table>
<tr>
<div>
<td>Hello</td>
<td>World</td>
</div>
</tr>
</table>
이 문제를 해결해주는 것이 Fragment
이다.
Columns
컴포넌트를 아래와 같이 변경하자.
class Columns extends React.Component {
render() {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
);
}
}
최상위 노드로 div
대신 React.Fragment
를 사용한 것을 볼 수 있다.
이렇게 되면 HTML 엘리먼트의 구조는 아래와 같아진다.
<table>
<tr>
<td>Hello</td>
<td>World</td>
</tr>
</table>
불필요하게 노드를 추가하는 일을 지양할 수 있게 해준다는 것을 확인 가능하다.
공식 문서에서는 Fragment
사용 시, key가 필요한 경우에는
반드시 React.Fragment
문법으로 명시적인 표시를 해줘야한다고 한다.
function Glossary(props) {
return (
<dl>
{props.items.map(item => (
// React는 `key`가 없으면 key warning을 발생합니다.
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
))}
</dl>
);
}
재밌게도 key 속성만이 React.Fragment
에 사용할 수 있는 유일한 속성이다.
이벤트 핸들러 등은 나중에 추가될 예정인 것 같다.(v18.2.0 공식 문서 기준)
이 부분도 앞서 살펴봤던 예시에 적용해보자.
사실..map()
을 사용한다면 key
속성을 반드시 넣어야한다.
그렇지 않으면 콘솔창에 에러를 보여줄 것이다.
즉, App.js
는
import ItrateComponent from "./IterateComponent";
export default function App() {
const arr = [1, 2, 3, 4, 5];
return (
<div>
{arr.map((num, index) => (
<ItrateComponent num={num} key={index} />
))}
</div>
);
}
이런 식으로 key 속성을 같이 적어주는 것이 옳은 방법이다.
음...이러면 React.Fragment
를 사용하는 느낌이 나지 않으니
ItrateComponent
를 App.js
에 직접 써보겠다.
import React from "react";
export default function App() {
const arr = [1, 2, 3, 4, 5];
return (
<div>
{arr.map((num, index) => (
<React.Fragment key={index}>
<h2>{num}번 반복 컴포넌트입니다.</h2>
<p>반복은 너무 귀찮다.</p>
</React.Fragment>
))}
</div>
);
}
import { Fragment } from "react";
export default function App() {
const arr = [1, 2, 3, 4, 5];
return (
<div>
{arr.map((num, index) => (
<Fragment key={index}>
<h2>{num}번 반복 컴포넌트입니다.</h2>
<p>반복은 너무 귀찮다.</p>
</Fragment>
))}
</div>
);
}
// 에러 발생!
export default function App() {
const arr = [1, 2, 3, 4, 5];
return (
<div>
{arr.map((num, index) => (
<key={index}>
<h2>{num}번 반복 컴포넌트입니다.</h2>
<p>반복은 너무 귀찮다.</p>
</>
))}
</div>
);
}
이런 식으로 활용 할 수 있겠다.
다만, 단축 문법에서는 key 속성을 사용할 수 없으니 이 점은 주의해야한다!
예시로 인한 혼동이 있을 수 있는데
map()
같은 반복 렌더링이 있을 때 써야된다는 것이 아니다.
최상위 노드(엘리먼트)를 불필요하게 만들어야할 상황에서 쓰는 것이다!
이 점을 혼동하지 말자.