안녕하세요. 추운 날들이 반복되고 있는데 다들 감기 조심하세요. 🤧🤧🤧
이번글은 React에서 children을 다룰 때 유용한 React.Children에 대해서 정리해 보려고 합니다.
사실 React 공식문서에서는 Children의 사용이 코드의 취약성(Fragile Code)로 이어질 수 있어 사용을 추천하지 않습니다.
하지만 오늘도 회사에서 발견했듯이 많은 레거시 코드들이 남아있으며,
당분간 계속 볼 거 같기에 Children API를 정리하려고 합니다.
추가적으로 글의 마지막 부분에서는 React 공식 문서에서 추천하는 Children을 사용하지 않는 방법 등을 작성할 예정입니다.
React Children API란?
React에서 정말 자주 사용하는 children과 Children API는 헷갈리기 쉬운데요.
React는 JSX라는 문법을 사용하여 컴포넌트를 작성하는데, 이러한 컴포넌트 사이에는 포함 관계를 설정할 수 있습니다.
이때, 상위 컴포넌트는 Parent Component, 하위 컴포넌트는 Child Component라고 부릅니다.
이러한 부모-자식 관계가 설정되면 부모 컴포넌트 내부에서는 children prop를 통해 자식 컴포넌트 정보에 접근할 수 있습니다.
## 예시
<ParentComponent>
<ChildComponent/>
</ParantComponent>
...
const ParantComponent =(props) =>{
return (
<>
{props.children}
</>
)
}
이렇게 자식 컴포넌트 정보에 접근할 때 문제는 이 children이 어떤 형식일지 모른다는 점입니다.
대표적으로 문자열, 숫자, HTML, React 컴포넌트, 함수, null 등등
이 다양한 children props의 형태를 효과적으로 안전하게 다룰 수 있는 방식이 React.Children API입니다.
React.Children은 map(), forEach(), count(), toArray(), only() 이렇게 5개의 함수로 이루어져 있으며
하나하나 살펴보겠습니다.
Children.count()
Children API의 count() 함수는 자식의 개수를 구할 때 사용합니다.
인자로 오직 children prop 하나만을 받으며, retunr 값으로 넘어온 children이 몇 개인지를 숫자 값으로 반환합니다.
이때, null, undefined, Booleans 같은 Empty Nodes 나 string, number, react node는 개별적으로 계산되며
배열은 개별 노드로 계산되지 않지만 배열의 자식은 계산에 포함됩니다.
## 예시
import { Children } from 'react';
function RowList({ children }) {
return (
<>
<h1>Total rows: {Children.count(children)}</h1>
...
</>
);
}
Children.forEach()
Children API의 forEach() 함수의 시그니쳐를 살펴보면,
Children.forEach(children, fn, thisArg?)로 많이 사용하는 Array.prototype.forEach()와 유사합니다.
Children.forEach 첫 번째 파라미터에 children이 들어가고 map 과는 다르게 return 값이 없음을 확인할 수 있으며,
주로 함수 외부에서 선언된 변수를 갱신하는 용도로 많이 사용됩니다.
## 예시
import { Children } from 'react';
function SeparatorList({ children }) {
const result = [];
Children.forEach(children, (child, index) => {
result.push(child);
result.push(<hr key={index} />);
});
// ...
Children.map()
Children API의 map() 함수 또한, Array.prototype.map()와 유사합니다.
Children.map(children, fn, thisArg?)의 시그니쳐를 가지고 있으며,
두 번째 인자로 주어지는 콜백 함수를 통해 각 자식을 다른 형태로 변환할 수 있습니다.
## 예시
import { Children } from 'react';
function RowList({ children }) {
return (
<div className="RowList">
{Children.map(children, child =>
<div className="Row">
{child}
</div>
)}
</div>
);
}
Children.only()
Children API의 only() 함수는 컴포넌트에 자식이 하나만 넘어왔는지 검증하고 싶을 때 사용할 수 있습니다.
만약 자식이 없거나 여러 개의 자식이 넘어왔다면 아래와 같은 오류가 발생시켜 알려줍니다.
React.Children.only expected to receive a single React element child.
## 예시
function Box({ children }) {
const element = Children.only(children);
// ...
Children.toArray
Children API의 toArray() 함수는 children을 일반 자바스크립트 배열로 변환해 주는 역할을 하며
자식을 상대로 join(), reverse(), sort(), filter(), reduce()와 같은 함수를 사용하고 싶을 때 유용하게 사용합니다.
특히, 아래의 배열 같이 children의 배열의 깊이가 깊거나 flat 한 배열로 변환하고 싶을 때 자주 사용합니다.
[
Object1, // banana
[
Object2, // apple
Object3, // kiwi
],
];
--- React.Children.toArray(children) ---
[
Object1, // banana
Object2, // apple
Object3, // kiwi
];
## 예시
import { Children } from 'react';
export default function ReversedList({ children }) {
const result = Children.toArray(children);
result.reverse();
// ...
Children 덜 사용하기
공식문서에 따르면 보통 JSX의 컴포넌트에 children을 전달할 때 일반적으로 컴포넌트가 개별 하위 항목을 조작하거나 변환할 것으로 기대하지 않기 때문에 Children을 사용하는 코드가 취약해지는 경우가 많다고 합니다.
때문에, 가능한 Children 메소드를 사용하지 않아야 하며 아래 예시처럼 각각의 자식을 수동으로 래핑 하는 방식을 추천하고 있습니다.
import { RowList, Row } from './RowList.js';
export default function App() {
return (
<RowList>
<Row>
<p>This is the first item.</p>
</Row>
<Row>
<p>This is the second item.</p>
</Row>
<Row>
<p>This is the third item.</p>
</Row>
</RowList>
);
}
이 방식은 Children.map을 사용하는 것과 달리 자동으로 모든 자식을 감싸진 않지만,
각각의 구성 요소를 추출해도 (변경해도) 이전과 같은 동작을 유지하기 코드의 변경에 용의 합니다.
import { RowList, Row } from './RowList.js';
export default function App() {
return (
<RowList>
<Row>
<p>This is the first item.</p>
</Row>
<MoreRows />
</RowList>
);
}
function MoreRows() {
return (
<>
<Row>
<p>This is the second item.</p>
</Row>
<Row>
<p>This is the third item.</p>
</Row>
</>
);
}
다른 방법으로 명시적으로 배열을 직접적으로 prop으로 전달할 수 있으며
import { RowList, Row } from './RowList.js';
export default function App() {
return (
<RowList rows={[
{ id: 'first', content: <p>This is the first item.</p> },
{ id: 'second', content: <p>This is the second item.</p> },
{ id: 'third', content: <p>This is the third item.</p> }
]} />
);
}
이때 row는 일반 자바스크립트 배열이기 때문에 RowList 구성 요소는 map과 같은 내장 배열 방식을 사용할 수 있습니다.
export function RowList({ rows }) {
return (
<div className="RowList">
{rows.map(row => (
<div className="Row" key={row.id}>
{row.content}
</div>
))}
</div>
);
}
또 다른 방법으로는 각각의 아이템을 생성하는 방법 대신에 return JSX 하는 함수를 전달하고 필요할 때에 호출하는 방법도 있습니다.
import TabSwitcher from './TabSwitcher.js';
export default function App() {
return (
<TabSwitcher
tabIds={['first', 'second', 'third']}
getHeader={tabId => {
return tabId[0].toUpperCase() + tabId.slice(1);
}}
renderContent={tabId => {
return <p>This is the {tabId} item.</p>;
}}
/>
);
}
import { useState } from 'react';
export default function TabSwitcher({ tabIds, getHeader, renderContent }) {
const [selectedId, setSelectedId] = useState(tabIds[0]);
return (
<>
{tabIds.map((tabId) => (
<button
key={tabId}
onClick={() => setSelectedId(tabId)}
>
{getHeader(tabId)}
</button>
))}
<hr />
<div key={selectedId}>
<h3>{getHeader(selectedId)}</h3>
{renderContent(selectedId)}
</div>
</>
);
}
여기서 renderContent과 같은 prop은 사용자 인터페이스의 일부를 렌더링 하는 방법을 지정하는 prop이기 때문에 render prop이라고 불립니다.
끝!
예전 공식문서 : https://react.dev/reference/react/Children
대안 공식문서 : https://react.dev/reference/react/Children#alternatives
'WEB > React, JS, TS (Web)' 카테고리의 다른 글
[JS] 고인물처럼 로그 찍기 (2) | 2024.03.12 |
---|---|
React 개발자를 위한 Google Ad Manager 반응형 광고 연동하기 (1) | 2024.03.06 |
Lighthouse로 서비스 성능 개선하기 (0) | 2023.11.13 |
SSH 키를 사용할 때마다 passphrase를 입력 안하도록 설정하기 (0) | 2023.10.31 |
iframe에서 GA4(gtag) 이벤트 추가하기 (부제: Outlook Add-in에서 GA4 이벤트 추가하기) (0) | 2023.10.01 |