본문 바로가기

FrontEnd

[React] 리액트 Children, React Children API의 모든 것

반응형
Children API를 사용하면 children prop으로 받은 JSX를 조작하고 변형할 수 있습니다.
const mappedChildren = Children.map(children, child =>
  <div className="Row">
    {child}
  </div>
);

Children API의 명세

Children.count(children)

Children.count(children)를 호출하여 children 데이터 구조의 자식 수를 계산합니다.
import { Children } from 'react';

function RowList({ children }) {
  return (
    <>
      <h1>Total rows: {Children.count(children)}</h1>
      ...
    </>
  );
}

파라미터

  • children: 컴포넌트가 받은 children prop의 값.

리턴값

  • children 내부의 노드 수

주의 사항

  • 빈 노드(null, undefined, Boolean), string, number 및 React 요소는 개별 노드로 계산됩니다.
  • 배열 자료구조 자체는 개별 노드로 계산되지 않지만 자식 노드는 개별 노드로 계산합니다.
  • 자식 노드에 대한 순회 깊이는 React 요소의 최대 깊이보다 깊을 수 없습니다.
    • 순회 중 React 요소를 렌더링하지 않으며, 따라서 해당 React 요소가 렌더링하는 하위 항목을 순회하며 갯수를 게산하지 않습니다.
    • 또한 Fragment도 순회하지 않습니다.

아래와 같이 예제를 작성해 보았습니다.

  • 일반적인 루트가 하나인 JSX는 1로 계산됩니다.
    • React 요소 하나(Fragment 또한)는 하나로 count 하기 때문입니다.
    • 리액트 요소는 렌더링 되지 않으며, 리액트 요소의 자식 또한 순회하지 않기 떄문입니다.
  • 배열의 경우, 중첩 배열에 flatMap을 적용한 배열의 길이가 결과입니다.
    • 배열 자체는 세지 않습니다
    • 배열 안에 있는 각 리액트 요소, 빈 노드만 하나로 계산합니다.

Children.forEach(children, fn, thisArg?)

Children.forEach(children, fn, thisArg?)를 호출하여 children 데이터 구조의 각 자식에 대해 일부 코드를 실행합니다.
import { Children } from 'react';

function RowList({ children }) {
  return (
    <>
      <h1>Total rows: {Children.count(children)}</h1>
      ...
    </>
  );
}

파라미터

  • children : 컴포넌트가 받은 children prop의 값.
  •  fn : 배열 forEach 메서드 콜백과 유사하게 각 자식에 대해 실행하려는 함수입니다.
    • 자식을 첫 번째 인수로, 인덱스를 두 번째 인수로 사용하여 호출됩니다. 인덱스는 0에서 시작하여 호출할 때마다 증가합니다.
  • (optional) thisArg : fn 함수에 바인딩할 this 입니다. 생략하면 undefined 입니다.

리턴값

  • children.forEach는 undefined를 반환합니다.

주의 사항

  • 빈 노드(null, undefined, Boolean), string, number 및 React 요소는는 개별 노드로 계산됩니다.
  • 배열 자료구조 자체는 개별 노드로 계산되지 않지만 자식 노드는 개별 노드로 계산합니다.
  • 자식 노드에 대한 순회 깊이는 React 요소의 최대 깊이보다 깊을 수 없습니다.
    • 순회 중 React 요소를 렌더링하지 않으며, 따라서 해당 React 요소가 렌더링하는 하위 항목을 순회하며 갯수를 게산하지 않습니다.
    • 또한 Fragment도 순회하지 않습니다.
  • fn를 이용해 키가 있는 요소 또는 요소 배열을 반환하면 반환된 요소의 키가 자식의 해당 원본 항목 키와 자동으로 결합됩니다.
    • 즉, 배열의 fn에서 여러 요소를 반환할 때 긱 요소의 키는 해당 함수 컨텍스트 내에서 고유해야 합니다.

참고:

리액트 요소는 아래와 같은 객체일 뿐입니다.

컴포넌트를 렌더링하거나 돔을 만들지 않습니다.

// 아래 호출은
<Greeting name="Taylor" />
// 아래와 유사한 객체를 만듭니다.
{
  type: Greeting,
  props: {
    name: 'Taylor'
  },
  key: null,
  ref: null,
}

Children.only(children)

Children.only(children)를 호출하여 자식이 단일 React 요소를 나타내는지 확인합니다.
function Box({ children }) {
  const element = Children.only(children);
  // ...

파라미터

  • children : 컴포넌트가 받은 children prop의 값.

리턴값

  • childrend이 유효한 리액트 요소이면, 해당 요소를 반환합니다.
  • 그렇지 않으면 에러를 throw 합니다.

주의 사항

이 메서드는 배열(예: Children.map의 반환 값)을 자식으로 전달하는 경우 항상 에러를 throw 합니다.
즉, children을 단일 요소만을 포함한 배열이 아니라 단일 React 요소로 강제합니다.

Children.toArray(children)

Children.toArray(children)를 호출하여 자식 데이터 구조를 배열로 만듭니다.
import { Children } from 'react';

export default function ReversedList({ children }) {
  const result = Children.toArray(children);
  result.reverse();
  // ...

파라미터

  • children : 컴포넌트가 받은 children prop의 값.

리턴값

자식 요소의 플랫 배열을 반환합니다.

  • 만약 자식이 중첩 배열이면 중첩을 전부 해체한 배열입니다.

주의 사항

빈 노드(null, undefined 및 Boolean)는 반환된 배열에서 생략됩니다.
반환된 요소의 키는 원래 요소의 키와 중첩 수준 및 위치에서 계산됩니다.
따라서 배열을 평탄화해도 동작이 변경되지 않습니다.
(평탄화 한것과 안한것과 관찰되는 동작은 동일함을 의미)

사용법

children 변형

컴포넌트가 children prop으로 받는 children JSX를 변환하려면 Children.map을 호출하세요.
import { Children } from 'react';

function RowList({ children }) {
  return (
    <div className="RowList">
      {Children.map(children, child =>
        <div className="Row">
          {child}
        </div>
      )}
    </div>
  );
}

위의 예에서 RowList는 받는 모든 children을 <div className="Row"> 컨테이너로 래핑합니다.
예를 들어 부모 컴포넌트가 세 개의 <p> 태그를 children prop으로 RowList에 전달한다고 가정해 보겠습니다.

<RowList>
  <p>This is the first item.</p>
  <p>This is the second item.</p>
  <p>This is the third item.</p>
</RowList>
그런 다음 위의 RowList 구현으로 최종 렌더링된 결과는 다음과 같습니다.
<div className="RowList">
  <div className="Row">
    <p>This is the first item.</p>
  </div>
  <div className="Row">
    <p>This is the second item.</p>
  </div>
  <div className="Row">
    <p>This is the third item.</p>
  </div>
Children.map은 map()을 사용하여 배열을 변환하는 것과 유사합니다. (transforming arrays with map())
차이점은 children 데이터 구조는 불투명(opaque) 것으로 간주된다는 것입니다.
즉, children 자료구조는 때로는 배열일 수 있으나, 배열이나 다른 특정 데이터 타입이라고 가정해서는 안 됩니다.
이것이 변환이 필요한 경우 Children.map을 사용해야 하는 이유입니다.

왜 children prop이 항상 배열이 아니죠?

React에서 children prop은 불투명(opaque)한 데이터 구조로 간주됩니다.
즉, children의 구조에 의존하여 로직을 적용하면 안됩니다.
자식을 변환, 필터링 또는 계산하려면 Children 메서드를 사용해야 합니다.

실제로 children 데이터 구조는 내부적으로 배열로 표현되는 경우가 많습니다.
그러나 자식이 하나만 있는 경우 불필요한 메모리 오버헤드가 발생하므로 React는 추가 배열을 만들지 않습니다.
children prop을 직접 검사하는 대신 Children 메서드를 사용하는 한
React가 데이터 구조의 실제 구현 방식을 변경하더라도 코드가 깨지지 않습니다.
 
 
children이 배열인 경우에도 Children.map에는 유용한 특수 동작이 있습니다.
예를 들어 Children.map은 반환된 요소의 키를 전달한 자식의 키와 결합합니다.
(주 : 원본 JSX 자식의 키를 바꾸는게 아니라, map 함수 내부에서 새로 key를 핸들링)
이는 원본 JSX 자식이 키를 잃어버리지 않음을 의미합니다.
 
참고로 아래와 같이 하면 key 충돌을 발생시킬 수 있습니다.
원본 키가 유지되기 때문입니다.
import { Children, Fragment } from "react";

function RowList({ children }) {
  let key = 0;
  return (
    <div className="RowList">
      {Children.map(children, (child) => [
        <Fragment key={++key + ""}></Fragment>,
        child
      ])}
    </div>
  );
}

export default function App() {
  return (
    <RowList>
      <MyComp key="1">This is the first item.</MyComp>
      <MyComp key="2">This is the second item.</MyComp>
      <MyComp key="3">This is the third item.</MyComp>
    </RowList>
  );
}

const MyComp = ({ children, ...rest }) => <p {...rest}>{children}</p>;

children data 구조는 렌더린됭 결과를 의미하지 않습니다.

자식 데이터 구조에는 JSX로 전달하는 컴포넌트의 렌더링된 출력이 존재하는 것이 아닙니다.
아래 예에서 RowList가 파라미터로 받은 children에는 3개가 아닌 2개의 항목만 포함됩니다.

  1. <p>This is the first item.</p>
  2. <MoreRows />

children을 조작해서 <MoreRows />와 같은 내부 컴포넌트의 렌더링된 출력을 가져올 방법은 없습니다.

이것이 일반적으로 대체 솔루션 중 하나를 사용하는 것이 더 나은 이유입니다.

각 자식(child)에 대해 코드 실행

Children.forEach를 호출하여 자식 데이터 구조의 각 자식을 반복합니다.
해당 메서드는 값을 반환하지 않으며 array forEach 메서드와 유사합니다.
이를 사용하여 고유한 배열 구성과 같은 사용자 지정 로직을 실행할 수 있습니다.

앞서 언급했듯이 children을 조작할 때 내부 컴포넌트의 렌더링된 출력을 얻을 수 있는 방법은 없습니다.
이것이 일반적으로 대체 솔루션 중 하나를 사용하는 것이 더 나은 이유입니다.

각 자식(child)을 배열로 변환

Children.toArray(children)를 호출하여 자식 데이터 구조를 일반 JavaScript 배열로 전환합니다.
이를 통해 필터, 정렬 또는 반전과 같은 내장 배열 메서드를 사용하여 배열을 조작할 수 있습니다.


Children API의 대안

이 섹션에서는 다음과 같이 가져온 Children API(대문자 C 사용)에 대한 대안을 설명합니다.
import { Children } from 'react';

children prop 사용과 혼동하지 마세요. children prop를 사용하는 것은 매우 좋고 권장합니다.

여러 컴포넌트 노출

Children 메서드를 사용하여 자식을 조작하면 종종 깨지기 쉬운 코드가 생성됩니다.
JSX에서 컴포넌트에 자식을 전달할 때 일반적으로 컴포넌트가 개별 자식을 조작하거나 변환할 것이라고 기대하지 않습니다.

 

가능하면 Children 메서드를 사용하지 마세요.
예를 들어 RowList의 모든 자식을 <div className="Row">로 래핑하려면
Row 컴포넌트를 내보내고 다음과 같이 모든 행을 수동으로 래핑합니다.

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을 사용하는 것과 달리 이 접근 방식은 모든 자식을 자동으로 래핑하지 않습니다.
그러나 이 접근 방식은 더 많은 컴포넌트를 계속 추출하더라도 작동하기 때문에
Children.map을 사용하는 이전 예제와 비교할 때 상당한 이점이 있습니다.
예를 들어 우리만의 MoreRows 컴포넌트를 추출해도 여전히 동작합니다.
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>
    </>
  );
}​
 
이것은 <MoreRows />를 단일 자식 컴포넌트(및 단일 행)로 "인식"하는  Children.map에서는 작동하지 않습니다.

위 코드 렌더링 결과

객체를 prop으로 전달받기

배열을 prop으로 명시적으로 전달할 수도 있습니다. 예를 들어 이 RowList는 rows 배열을 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는 일반 JavaScript 배열이므로 RowList 컴포넌트는 map과 같은 내장 배열 메서드를 사용할 수 있습니다.
이 패턴은 구조화된 데이터로 children에 대해 더 많은 정보를 전달하길 원할 때 유용합니다.
아래 예제에서 TabSwitcher 컴포넌트는 객체 배열을 tabs 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> }
    ]} />
  );
}
 

자식을 JSX로 전달하는 것과 달리 이 접근 방식을 사용하면 각 탭에 헤더와 같은 추가 컴포넌트를 전달할 수 있습니다.
tabs 자료구조로로 직접 작업하고 있으며 JS 배열이기 때문에 Children 메서드가 필요하지 않습니다.

render props 패턴 사용하기

모든 단일 항목에 대해 JSX를 생성하는 대신 JSX를 반환하는 함수를 전달하여 필요할 때 해당 함수를 호출할 수도 있습니다.
이 예에서 App 컴포넌트는 renderContent 함수를 TabSwitcher 컴포넌트에 전달합니다.
TabSwitcher 컴포넌트는 선택한 탭에 대해서만 renderContent를 호출합니다. 

App.js

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>;
      }}
    />
  );
}

Tabswitcher.js

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이라고 합니다.
특별한 것은 없습니다. 함수일 뿐인 일반적인 prop입니다.

 

render prop은 함수이므로 정보를 전달할 수 있습니다.
예를 들어 이 RowList 컴포넌트는 각 행의 ID와 인덱스를 renderRow render prop에 전달합니다.
이 prop은 인덱스를 사용하여 짝수 행을 강조 표시합니다.

App.js

import { RowList, Row } from './RowList.js';

export default function App() {
  return (
    <RowList
      rowIds={['first', 'second', 'third']}
      renderRow={(id, index) => {
        return (
          <Row isHighlighted={index % 2 === 0}>
            <p>This is the {id} item.</p>
          </Row> 
        );
      }}
    />
  );
}

RowList.js

import { Fragment } from 'react';

export function RowList({ rowIds, renderRow }) {
  return (
    <div className="RowList">
      <h1 className="RowListHeader">
        Total rows: {rowIds.length}
      </h1>
      {rowIds.map((rowId, index) =>
        <Fragment key={rowId}>
          {renderRow(rowId, index)}
        </Fragment>
      )}
    </div>
  );
}

export function Row({ children, isHighlighted }) {
  return (
    <div className={[
      'Row',
      isHighlighted ? 'RowHighlighted' : ''
    ].join(' ')}>
      {children}
    </div>
  );
}
이것은 부모가 자식을 조작하지 않고 어떻게 협력할 수 있는지에 대한 예입니다.

문제 해결

커스텀 컴포넌트를 전달했지만 Children 메서드가 렌더링 결과를 표시하지 않습니다.

다음과 같이 두 개의 자식을 RowList에 전달한다고 가정합니다.

<RowList>
  <p>First item</p>
  <MoreRows />
</RowList>
RowList 내에서 Children.count(children)를 수행하면 2를 얻습니다.
MoreRows가 10개의 다른 항목을 렌더링하거나 null을 반환하더라도 Children.count(children)는 여전히 2입니다.
Children.count(children) 메서드는 RowList의 관점에서 수신한 JSX를 봅니다.
즉, MoreRows 컴포넌트의 내부를 "들여다보지" 않습니다. (ex MoreRows 객체의 children prop을 보지 않음)
 
이 한계는 컴포넌트를 추출하여 활용하기 어렵게 합니다.
따라서 위에서 설명한 대안(컴포지션)을 사용하는 것을 추천합니다.

Reference

https://beta.reactjs.org/reference/react/Children

 

Children

A JavaScript library for building user interfaces

beta.reactjs.org

 

 

반응형