GitHub
©-2 kd02109 All rights reserved.

React children

  • react
August 21, 2023

1. React Children

리엑트에서 컴포넌트 간의 연결은 prop을 통해 이루어집니다. 이는 단방향 바인딩을 특징으로 합니다. prop을 통한 컴포넌트의 수직적 연결은 두 가지 방법이 있습니다.

  1. 일반적인 prop 전달하기
  • 일반적으로 가장 많이 사용하는 방법이라고 생각합니다.
function Avatar() {
  return (
    <img
      className="avatar"
      src="https://i.imgur.com/1bX5QH6.jpg"
      alt="Lin Lanying"
      width={100}
      height={100}
    />
  );
}
 
export default function App() {
  return <Avatar />;
}
  1. React Children
  • React Children은 HTML의 빌트인 태그 특징을 그대로 도입하였습니다.
<select>
  <options></options>
</select>

이러한 특징을 바탕으로 직접 작성한 컴포넌트들끼리 중첩할 수 있습니다. jsx 태그 내에 컴포넌트를 중첩하면 리엑트에서 부모 컴포넌트는 children이라는 prop을 받게 됩니다.

import Avatar from './Avatar.js';
 
function Card({ children }: {children: React.ReactNode}) {
  return (
    <div className="card">
      {children}
    </div>
  );
}
 
export default function Profile() {
  return (
    <Card>
      <Avatar
        size={100}
        person={{
          name: 'Katsuko Saruhashi',
          imageId: 'YfeOqp2'
        }}
      />
    </Card>
  );
}

2. 모든 것은 child가 될 수 있습니다.

recat의 children에는 원시 값, 참조 값과 jsx 값 전달이 모두 가능합니다. 이는 React.ReactNode의 타입을 확인해 보면 명확하게 알 수 있습니다.

type ReactNode =
  | ReactElement
  | string
  | number
  | ReactFragment
  | ReactPortal
  | boolean
  | null
  | undefined
  | DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES[keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES];

이를 바탕으로 children에는 어떤 타입이든 올 수 있다는 것을 확인할 수 있습니다.

function Grid({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>;
}
 
export default function App() {
  const foods = ['라면', '밥', '김치', '김'];
 
  return (
    <div>
      {/* 문자 가능 */}
      <Grid>Hello</Grid>
 
      {/* 배열 가능 */}
      <Grid>
        {foods.map(item => (
          <div key={item}>{item}</div>
        ))}
      </Grid>
 
      {/* jsx 값 가능, 및 혼합해서 전달 가능 */}
      <Grid>
        <div
          style={{
            width: '100px',
            height: '100px',
            color: 'white',
            backgroundColor: 'black',
          }}>
          {"It's a jsx"}
        </div>
        {"it's a string"}
      </Grid>
    </div>
  );
}

2-1. children에 함수 전달하기

이러한 방법 뿐만 아니라 children에 특정 값을 반환하는 함수 자체를 전달해줄 수도 있습니다. 이는 특정한 디자인 패턴으로 정리해서 각 컴포넌트의 역할을 명확하게 분리할 수 있습니다. Function as Child 패턴으로 불리기도 합니다.

react children은 자식에 어떤 것이 들어올지 모른다고 가정을 합니다. FunctionComponent는 데이터 로직만을 갖습니다. 해당 데이터 로직을 자식 함수에 주입합니다.

children에서 필요한 데이터를 전달 받아서 같은 데이터를 활용하는 다른 jsx component에 동일한 로직을 적용할 수 있습니다.

고양이 랜덤 이미지를 가지고 오는 API를 예시로 활용해 보았습니다.

import { useEffect, useState } from 'react';
 
// API 타입 지정
type Api = {
  id: string;
  url: string;
  width: number;
  height: number;
};
 
// FunctionComponent prop 설정
type Prop = {
  url: string;
  children: (image: string) => JSX.Element;
};
 
// API 불러오는 함수 설정
async function fetchUrl<T>(url: string) {
  const json = await fetch(url);
  const data: Promise<T[]> = json.json();
  return data;
}
 
// 데이터를 받아오는 로직 작성
function FunctionComponent({ children, url }: Prop) {
  const [image, setImage] = useState('');
 
  useEffect(() => {
    (async () => {
      const data = await fetchUrl<Api>(url);
      setImage(data[0].url);
    })();
  }, [url]);
 
  return children(image);
}
 
export default function App() {
  return (
    <FunctionComponent url={'https://api.thecatapi.com/v1/images/search'}>
      {/* 자유롭게 필요한 jsx Component 로직을 작성할 수 있습니다. */}
      {image => (
        <div style={{ display: 'flex', flexDirection: 'column' }}>
          <img src={image} width={'500px'} height={'500px'} />
          <span>Image URL은 다음과 같습니다. ({image})</span>
        </div>
      )}
    </FunctionComponent>
  );
}

이 외에도 Compounds Components와 같은 패턴에서 children을 적극적으로 활용하고 있습니다. Compounds Components를 더 자세히 알고 싶다면 길종님의 정리 혹은 해당 블로그를 참조해주세요

3. Children API 활용하기

react의 Children API를 통해 각 컴포넌트에 들어오는 children을 조작할 수 있습니다. 하지만 공식문서에서 추천하지 않는 기능입니다. 공식 문서에는 다음과 같이 설명하고 있습니다.

Children을 사용해야 하는 경우는 드물고, 코드가 취약해질 수 있습니다. 다른 일반적인 대안을 참조하세요.

  • 명확한 이유는 알려주지 않고 있습니다. 🥲 이를 알기 위해서는 직접 라이브러리를 까보는 수 밖에 없는 것 같습니다.

그래도 알면 나쁘지 않다고 생각하기에 기초적인 몇몇 method만을 소개합니다.

Children.toArray(children)

  • children 데이터 구조에서 배열을 생성할 수 있습니다.

Children.forEach(children, fn, thisArg?) && Children.map(children, fn, thisArg?)

  • 자바스크립트의 Array.prototype.forEach(), Array.prototype.map()과 사용법이 유사합니다.

예시 첫 번째 children 요소 랜더링에서 배제하기

type PropChildren = {
  children : React.ReactNode
}
 
// Children.toArray(children)
function IgnoreFirstChild({children}: PropChildren): any{
  const childrenList = Children.toArray(children)
  return childrenList.map((item, index) => {
    if(index === 0) return null
    else return (<div>{item}</div>)
  } )
}
 
// Children.toArray(children)
function IgnoreFirstChildCase2({children}: PropChildren): any {
  return Children.map(children, (child, index)=> {
    if(index === 0) return null
    else return <div>{child}</div>
  })
}
 
export default function App() {
  return (
    <>
      <IgnoreFirstChild>
        <div >First</div> {/* 렌더링 제외*/}
        <div >Second</div>
      </IgnoreFirstChild>
      <IgnoreFirstChildCase2>
        <div>First</div> {/* 렌더링 제외*/}
        <div>Second</div>
      </IgnoreFirstChildCase2>
    </>
  );
}
 

Reference

  • 공식문서

  • 공식문서 번역 페이지

  • react-children-deepdive

  • react-children-tip

  • 우아한 테크 코스

Content Table
  • 1. React Children
  • 2. 모든 것은 child가 될 수 있습니다.
  •    - 2-1. children에 함수 전달하기
  • 3. Children API 활용하기
  • Reference
React의 디자인 패턴
Ref를 활용한 참조값