Logo

React의 Children API 사용법

이번 포스팅에서는 React의 Children API를 사용해서 컴포넌트의 children prop을 다루는 방법에 대해서 다뤄보겠습니다.

Children API의 필요성

먼저 React에서 Children라는 API가 왜 필요한지에 대해서 간단하게 짚고 넘어가겠습니다.

우선 이름이 비슷해서 컴포넌트의 children prop과 Children API이 헛갈리게 쉬운데요. 소문자로 시작하는 children은 소외 props라고 일컫는 컴포넌트 함수의 매개 변수가 가지고 있는 하나의 속성이며 이를 통해 컴포넌트의 자식이 넘어오게 됩니다. 대문자로 시작하는 Children는 React에서 children prop을 효과적으로 다룰 수 있도록 제공하는 API 입니다.

React를 좀 써보신 분이라면 자식(children) prop을 상대로 어떤 작업을 하는 것이 생각보다 까다롭다는 것을 잘 아실텐데요. 이에 대해서 간단히 설명을 드리면…

children prop으로 이렇게 문자열이나 숫자가 넘어올 수도 있고요.

<OurComponent>Text</OurComponent>
<OurComponent>{100}</OurComponent>

HTML 요소나 React 요소가 넘어올 수도 있고요.

<OurComponent>
  <input />
</OurComponent>
<OurComponent>
  <Button />
</OurComponent>

함수가 넘어올 수도 있고, (보통 function as children 또는 render prop이라고 하죠?)

<OurComponent>{(data) => <span>{data}</span>}</OurComponent>

아니면 아무 것도 넘어오지 않을 수도 있습니다. (null)

<OurComponent />

심지어 이것들이 뒤죽박죽 섞인 배열일 수도 있죠 😆

<OurComponent>
  <input />
  Text
  <Button />
  {100}
</OurComponent>

이처럼 React는 컴포넌트의 자식으로 무엇인든지 사용할 수 있도록 상당히 유연하게 설계되어 있기 때문에 우리는 children prop의 자료형(data type)을 예상할 수 없습니다. 그러므로 children prop을 상대로 직접 프로그래밍하게 되면 버그가 발생하기 쉬워집니다.

이것이 React에서 Children이라는 별도의 API를 제공하는 이유이며, 우리는 Children API를 통해서 children prop을 좀 더 안전하게 다룰 수 있습니다.

Children API 접근 방법

React의 Children API는 크게 두 가지 방법으로 접근할 수 있습니다. (당연히 본인 프로젝트에 react 패키지가 이미 설치되어 있어야겠죠?)

첫 번째 방법은 react 패키지로 부터 React를 불러온 후에, React.Children에 접근하는 것입니다. 한번 컴포넌트 안에서 React.Children를 콘솔에 출력해볼까요?

import React from "react";

function ReactChildren({ children }) {
  console.log(React.Children);
  return <>ReactChildren</>;
}

출력 결과를 보면 React.Childrenmap(), forEach(), count(), toArray(), only() 이렇게 5개의 함수로 이루어졌다는 것을 알 수 있습니다.

{map: ƒ, forEach: ƒ, count: ƒ, toArray: ƒ, only: ƒ}

두 번째 방법은 react 패키지로 부터 Children를 named import로 바로 불러와서 사용하는 것입니다.

import { Children } from "react";

function ReactChildren({ children }) {
  console.log(Children);
  return <>ReactChildren</>;
}

콘솔에 동일한 내용이 출력될 것입니다.

{map: ƒ, forEach: ƒ, count: ƒ, toArray: ƒ, only: ƒ}

저는 개인적으로 첫 번째 방식을 선호하는데요. 두 번째 방식은 타이핑을 적게 할 수 있지만 Childrenchildren이 너무 비슷해서 코드를 자세히 들여다보지 않으면 실수하기가 좋더라고요. 👀

그래서 본 포스팅에서는 첫 번째 방식으로 예제를 작성하도록 하겠습니다.

Children.map()

Children API에서 아마도 가장 많이 사용되는 함수는 map()일텐데요.

많은 분들이 자바스크립트 배열의 map() 함수를 떠올리실 거에요. 실제로 상당히 흡사한 API를 가지고 있습니다.

이 함수는 첫 번째 인자로는 children prop을 받고, 두 번째 인자로는 콜백 함수를 받는데요. 이 콜백 함수에는 각 자식과 인덱스가 인자로 주어지기 때문에, 우리는 이 콜백 함수를 통해서 각 자식을 다른 형태로 변환할 수 있습니다.

참고로 Children API의 map() 함수의 시그니처를 타입스크립트로 나타내보면 다음과 같습니다.

map(children: unknown, fn: (child: unknown, index: number) => unknown): unknown[]

예를 들어, 홀수 번째(짝수 인덱스) 자식의 글씨를 굵게 하고, 짝수 번째(홀수 인덱스) 자식에 밑줄을 그어주는 컴포넌트를 작성해볼까요?

function Map({ children }) {
  return React.Children.map(children, (child, i) =>
    i % 2 === 0 ? <b>{child}</b> : <u>{child}</u>
  );
}

이제 이 Map 컴포넌트의 자식으로 다음과 같은 6개의 <span> 요소를 사용해보면, AAA, CCC, EEE는 굵은 글씨로 표시되고, BBB, DDD, EEE에는 밑줄이 그어질 것입니다.

<Map>
  <span>AAA</span>
  <span>BBB</span>
  <span>CCC</span>
  <span>DDD</span>
  <span>EEE</span>
  <span>FFF</span>
</Map>

여기서 혹시 그냥 children prop을 상대로 바로 map() 함수를 호출하면 되지 않나 생각하실 수도 있을 것 같은데요. 위에서 설명드린 것 처럼 우리는 children prop의 자료형이 뭐가 될지 알 수 없습니다.

운좋게 배열일 수도 있겠지만 HTML/React 요소나 함수, 심지어 null이 될 수도 있습니다. Children.map()을 사용하면 children prop이 HTML/React 요소나 함수일 때는 마치 하나의 원소가 들어있는 배열처럼 처리를 해주고, null일 때는 빈 배열처럼 처리를 해줍니다. 따라서 항상 배열을 다루는 것처럼 안전하게 API를 쓸 수 있는 것이죠.

Children.forEach()

Children API의 forEach() 함수는 방금 살펴본 map() 함수와 거의 비슷하지만 두 번째 인자로 넘어가는 콜백 함수가 아무것도 반환하지 않는다는 차이가 있습니다. 그래서 함수의 시그니쳐를 타입스크립트로 나타내보면 다음과 같아지는데요.

forEach(children: unknown, fn: (child: unknown, index: number) => void): unknown[]

보통 forEach() 함수 외부에서 선언된 변수를 갱신하는 용도로 많이 사용되는데요.

예를 들어, 모든 자식이 담고 있는 문자열의 길이를 더해서 표시해주는 컴포넌트를 작성해보겠습니다.

function ForEach({ children }) {
  let count = 0;
  React.Children.forEach(children, (child) => {
    count += child.length;
  });
  return (
    <>
      {children}
      {`(총 글자수: ${count})`}
    </>
  );
}

작성한 ForEach 컴포넌트의 자식으로 다음과 같은 6개의 문자열을 사용해보면, 총 글자수: 18라는 메시지가 보이게 될 것입니다.

<ForEach>
  {"AAA"}
  {"BBB"}
  {"CCC"}
  {"DDD"}
  {"EEE"}
  {"FFF"}
</ForEach>

child.length 부분 때문에 모든 자식으로 반드시 문자열을 사용해야하는 점 주의바랍니다.

Children.count()

Children API의 count() 함수는 자식의 개수를 구할 때 사용하는데요. 인자로 children prop 하나만 받으며, 해당 컴포넌트로 넘어온 자식이 몇개인지를 반환합니다.

children prop이 반드시 배열이 아니더라도 Children.count() 함수는 안전하게 작동합니다. 즉, Children.count() 함수는 자식이 null이면 0을 반환하고, 자식이 하나 밖에 없으면 1을 반환합니다.

예를 들어, 자식의 개수를 화면에 추가로 보여주는 컴포넌트를 작성해볼까요?

function Count({ children }) {
  const count = React.Children.count(children);
  return (
    <>
      {children}
      {`(총 자식수: ${count})`}
    </>
  );
}

작성한 Count 컴포넌트의 자식으로 다음과 같은 6개의 <span> 요소를 사용해보면, 총 자식수: 6라는 메시지가 보일 것입니다.

<Count>
  <span>AAA</span>
  <span>BBB</span>
  <span>CCC</span>
  <span>DDD</span>
  <span>EEE</span>
  <span>FFF</span>
</Count>

Children.toArray()

Children API의 toArray() 함수는 자식을 일반 자바스크립트 배열로 변환해주는데요. 자식을 상대로 join(), reverse(), sort(), filter(), reduce()와 같은 자바스크립트 배열에서 제공하는 함수를 사용하고 싶을 때 유용합니다.

예를 들어, 홀수 번째(짝수 인덱스) 자식만 화면에 그려주는 컴포넌트를 작성해보겠습니다.

function ToArray({ children }) {
  const array = React.Children.toArray(children);
  return array.filter((child, i) => i % 2 === 0);
}

작성한 ToArray 컴포넌트의 자식으로 다음과 같은 6개의 문자열을 사용해보면, 화면에 AAA, CCC, EEE만 나타나게 될 거에요.

<ToArray>
  {"AAA"}
  {"BBB"}
  {"CCC"}
  {"DDD"}
  {"EEE"}
  {"FFF"}
</ToArray>

Children.only()

Children API의 only() 함수는 컴포넌트에 자식이 하나만 넘어왔는지 검증하고 싶을 때 사용할 수 있는데요. 만약에 자식이 없거나 여러 개의 자식이 넘어왔다면 다음과 같은 오류가 발생하기 때문입니다.

React.Children.only expected to receive a single React element child.

예를 들어, 자식이 하나가 아닌 경우에 오류를 발생시키는 컴포넌트를 작성해볼까요?

function Only({ children }) {
  return React.Children.only(children);
}

Only 컴포넌트는 자식이 하나일 때만 정상 작동할 것입니다.

/* 정상 작동 (AAA 표시) */
<Only>
  <span>AAA</span>
</Only>

/* 오류 */
<Only>
  <span>AAA</span>
  <span>BBB</span>
</Only>

/* 오류 */
<Only/>

/* 오류 (하나의 자식이지만 문자열임) */
<Only>AAA</Only>

위와 같이 자식이 하나라도 HTML 요소나 React 요소가 아니면 오류가 발생하는 부분에 유의 바랍니다.

전체 코드

본 포스팅에서 작성한 코드는 아래에서 직접 확인하고 실행해보실 수 있습니다.

마치면서

이상으로 React의 Children API에서 제공하는 5가지 함수를 어떻게 사용하는지 살펴보았습니다.

일반적으로 부모 컴포넌트가 자식 컴포넌트에 직접 접근하는 것은 프레임워크나 라이브러리 개발과 같은 특수한 사례를 제외하고는 모범 사례(best practice)로 여겨지지는 않습니다. 따라서 React의 Children API를 고려하기 전에 다른 대안은 없는지 항상 따져보시라고 당부드리고 싶습니다.