[React] React Children (feat. React.Children.toArray(children))

고병표·2022년 2월 4일
0

React.js

목록 보기
12/21

React.Children.toArray(children)

공식 문서 설명 : children불투명 데이터 구조를 각 자식에 할당된 키가 있는 평면 배열로 반환합니다 . 렌더링 메서드에서 자식 컬렉션을 조작하려는 경우, 특히 this.props.children전달하기 전에 재정렬하거나 슬라이스하려는 경우에 유용합니다.

react의 virtual dom은 key값으로 각각의 객체를 구분하고 그 객체의 props가 바뀌었는지 확인을 한다
Array.prototype.map을 사용해서 반복적인 컴포넌트를 그리는 경우에 React.Children.toArray()으로 감싸주면 자동으로 unique한 key가 지정이 된다.

React에서 가장 흔하게 볼 수 있는 prop을 물어본다면, 많은 이들이 children을 꼽지 않을까 싶은데요.
React의 강력한 합성(Composition) 모델을 구현하기 위해서는 children prop을 빼놓을 수 없기 때문일 겁니다.

export default function Alert ({children}) {
  return (
    <AlertRoot>
      ...
      <AlertMessage>
        {children}
      </AlertMessage>
    </AlertRoot>
  ):
}

보통의 경우에는 적당한 위치에 {children}을 스윽 넣어주면 되니 그 속을 자세히 들여다볼 필요가 없지만, children의 요소들을 각각 다르게 노출한다거나 sort, filter, slice 같은 조작이 필요한 경우에는 이야기가 달라집니다.

이 글에서는 children을 다룰 때 유용한 React.Children 유틸리티를 파헤쳐 보고, React.Children이 가진 결점을 보완할 방법에 대해 소개해 볼까 합니다.

React.Children.toArray()

function Box({ children }) {
  console.log(children);
  console.log(React.Children.toArray(children));
  return children;
}

const fruits = [
  { id: 1, name: "apple" },
  { id: 2, name: "kiwi" },
];

export default function App() {
  return (
    <Box>
      <div name="banana">banana</div>
      {fruits.map(fruit => (
        <div key={`${fruit.id}_${fruit.name}`} name={fruit.name}>
          {fruit.name}
        </div>
      ))}
    </Box>
  );
}

위 코드를 살펴볼까요.

Box 컴포넌트는 children과 React.Children.toArray(children) 두 값을 콘솔에 출력하는데, 두 값이 서로 다르다는 것을 확인할 수 있습니다.

* console.log(children)

[
  Object1, // banana
  [
    Object2, // apple
    Object3, // kiwi
  ],
];

* console.log(React.Children.toArray(children))

[
  Object1, // banana
  Object2, // apple
  Object3, // kiwi
];

실제로 DOM에는 { children }을 그냥 사용해도 두 번째 결과처럼 1depth로 렌더링 되기 때문에 착각하기 쉬운데요.
children 배열을 조작할 때 예상과 다른 결과를 얻을 수 있습니다.

function Box({ children }) {
  return children.slice(0, 2); // 두 개만 보이길 원했지만 실제로는 세 개가 보입니다.
}

const fruits = [
  { id: 1, name: "apple" },
  { id: 2, name: "kiwi" },
];

export default function App() {
  return (
    <Box>
      <div name="banana">banana</div>
      {fruits.map(fruit => (
        <div key={`${fruit.id}_${fruit.name}`} name={fruit.name}>
          {fruit.name}
        </div>
      ))}
    </Box>
  );
}

Children.toArray() 공식 문서에 이렇게 적혀있습니다.

Returns the children opaque data structure as a flat array with keys assigned to each child.

첫 번째 포인트는 위 예제에서 확인했듯이 children을 1차원 배열(flat array)로 변환해서 리턴해준다는 것인데요.
여기에 추가해서 children이 배열이 아닌 opaque data structure 일 때도 동작합니다.

function Box({ children }) {
  console.log(children); // object
  console.log(React.Children.toArray(children)); // array
  /**
   * children이 object이므로 slice 같은 배열 함수를 사용할 수 없습니다.
   * children.slice(0,2); // children.slice is not a function
   */
  return React.Children.toArray(children).slice(0, 2);
}

export default function App() {
  return (
    <Box>
      <div name="banana">banana</div>
    </Box>
  );
}
function Box({ children }) {
  console.log(children); // undefined
  console.log(React.Children.toArray(children)); // array
  /**
   * children이 undefined이므로 마찬가지로 배열 함수를 사용할 수 없습니다.
   * children.slice(0,2); // Cannot read property 'slice' of undefined
   */
  return React.Children.toArray(children).slice(0, 2);
}

export default function App() {
  return <Box />;
}

두 번째 포인트는 각 child 들에 고유한 key를 할당한다는 것입니다.
제일 먼저 Box 컴포넌트에서 콘솔에 출력한 내용을 펼쳐보면 다음과 같습니다.

  • console.log(children)의 child
{
  $$typeof: Symbol(react.element),
  key: null.
  props: {
    name: "banana",
    children: "banana"
  },
  ...
}
  • console.log(Children.toArray(children))의 child
{
  $$typeof: Symbol(react.element),
  key: ".0",
  props: {
    name: "banana",
    children: "banana"
  },
  ...
}

Children.toArray()는 1차원 배열(flat array)로 변환하는 과정에서 재조정(Reconciliation)과 렌더링 최적화를 위해 고유한 key 값을 부여합니다.

공식 문서를 좀 더 살펴보겠습니다.

That is, toArray prefixes each key in the returned array so that each element’s key is scoped to the input array containing it.

쉽게 이해하기 위해서 Box 컴포넌트를 아래와 같이 수정하고, 결과를 확인해 보겠습니다.

function Box({children}) {
  console.log(
    React.Children.toArray(children)
      .map(child => child.key)
      .join('\n')
  );

  return children;
}
...(생략)
.0          // banana
.1:$1_apple // apple
.1:$2_kiwi  // kiwi

콘솔에 출력된 fruit의 key를 보면, flatten 하기 전의 children에서 fruit 배열의 key 값인 .1이 공통 prefix로 사용된 것을 확인할 수 있습니다.

그 뒤로 fruit 배열 요소인 <div key={\\${fruit.id}_${fruit.name}\} name={fruit.name}>의 key 값이 $1_apple, $2_kiwi처럼 붙습니다.


하지만 React.Children.toArray()가 예상과 다르게 동작하는 경우가 있는데요,
기존 코드를 아래와 같이 수정하고 콘솔을 확인해 보겠습니다.

function Box({ children }) {
  console.log(React.Children.toArray(children));
  return children;
}

const fruits = [
  { id: 1, name: "apple" },
  { id: 2, name: "kiwi" },
];

export default function App() {
  return (
    <Box>
      <React.Fragment>
        <div name="banana">banana</div>
        <div name="melon">melon</div>
      </React.Fragment>
      {fruits.map(fruit => {
        return (<div key={fruit.id} name={fruit.name}>{fruit.name}</div>);
      })}
    </Box>
  );
}
[
  Object1, // banana & melon
  Object2, // apple
  Object3, // kiwi
];

React.Children.toArray()가 <React.Fragment>의 내부는 flatten 하지 않는 문제인데, 지금까지 나와 있는 해결책은 react-keyed-flatten-children를 사용하는 방법뿐입니다.

import flattenChildren from "react-keyed-flatten-children";

function Box({ children }) {
  console.log(flattenChildren(children));
  return children;
}
...

이 이슈와 관련한 코멘트에서 확인할 수 있듯이, React.Children 유틸리티들은 현재 maintenance mode이며, 추후에 다른 유틸로 대체될 수 있다고 합니다. React.Children에 대해 더 알고 싶다면 react-children-deepdive 문서를 참고하면 좋을 것 같습니다.

참고
react-children-deepdive
react-api#transforming-elements
kakao FE 기술블로그

0개의 댓글