[React] Context가 꼭 필요할까? 컴포넌트 합성으로 props drilling을 극복해보자

jangws·2022년 5월 8일
4

React

목록 보기
4/8

0. props drilling?

Prop Drilling은 props를 오로지 하위 컴포넌트로 전달하는 용도로만 쓰이는 컴포넌트들을 거치면서 React Component 트리의 한 부분에서 다른 부분으로 데이터를 전달하는 과정이다.
prop drilling이 보통 3~5개 컴포넌트를 거치는 정도이면, 괜찮을 수 있으나, 10개 이상을 거친다면 중간 컴포넌트들은 불필요하게 prop을 받게 되어 가독성이 떨어져 유지보수가 어려워진다.

이를 해결하기 위해 전역 상태관리 라이브러리를 사용할 수 있으며, 간단하게는 context api를 사용할 수 있다.

1. context란?

리액트에서 context를 쓰는 이유는 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공하기 위함이다.

context는 전역적(global)으로 데이터(현재 로그인한 유저, 테마, 선호 언어 등)를 공유할 수 있는 방법이다.

1-1. props를 여러 번 넘겨야 한다면 무조건 context?

리액트 공식문서에서는 context를 사용하면 컴포넌트를 재사용하기 어려워지므로 꼭 필요할 때만 쓸 것을 권장한다.

context보다 컴포넌트 합성(composition)이 더 간단한 해결책일 수 있다.

2. 합성(composition)

컴포넌트에서 다른 컴포넌트를 담는 것이 합성이다.
어떤 컴포넌트들은 어떤 자식 엘리먼트가 들어올 지 미리 예상할 수 없는 경우가 있다. 예를 들어, 모달 컴포넌트 등에서 찾아볼 수 있는데, 이러한 컴포넌트에서는 children prop을 사용하여 자식 엘리먼트를 출력에 전달할 수 있다.

부모 컴포넌트는 자신이 감싸고 있는 자식 엘리먼트들을{props.children}라고 적은 부분에 출력할 수 있다.

2-1. 컴포넌트 커스터마이징

단지 children을 그대로 출력하는 것에 그치지 않으며, 해당 children 리액트 엘리먼트들의 속성을 바꿔서 반환한 새 엘리먼트를 출력에 전달할 수도 있다.

즉, 중요한 것은 부모 컴포넌트가 합성하고 싶은 엘리먼트들을 감싸면서, 자신의 props로서 감쌌던 자식 엘리먼트들을 children이라는 prop으로 받아서 객체로서 사용할 수 있다는 것이다.

// 예시 코드 : 자식 input들의 onChange 콜백함수를 합성하는 ParentBox
import React from 'react';

const ParentBox = ({ children }) => {
  const flattenChildren = React.Children.toArray(children);

  const parentFunc = (e) => {
    console.log(e, '여기에서는 자식 인풋 onChange 이벤트에 추가할 함수를 작성할 수 있다');
  };

  const compositeOnChange = (callback) => (e) => {
    callback(e);
    parentFunc(e);
  };

  const changedChildren = flattenChildren.map((child) => {
    if (child.type?.target === 'input') {
      return {
        ...child,
        props: {
          ...child.props,
          onChange: compositeOnChange(child.props.onChange),
        },
      };
    }
    return child;
  });

  return <>{changedChildren}</>;
};

export default ParentBox;
// 사용
<ParentBox>
  <input onChange={onChangeInput} />
  <input onChange={onChangeInput} />
  <input onChange={onChangeInput} />
</ParentBox>;

이처럼 자식 이벤트 콜백함수에 부모가 주는 콜백함수를 합성할 수 있다.

React.Children.toArray

참고로 위 코드에서 React.Children.toArray(children) 메서드는 children을 평평하게 만들어 1차원 배열(flat array)로 변환해서 리턴한다. children의 집합을 다루고 싶을 때 유용하며, 배열이 아닌 opaque data structure일 때도 동작한다.opaque data structure가 의미하는 바는, children이 어배열, 함수, 객체 등 어떤 타입이어도 괜찮다는 것이다. 특히, children을 하부로 전달하기 전에 다시 정렬하거나 일부만 잘라내고 싶을 때 유용하다.
또한, 이 메서드는 각 child 들에 고유한 key를 할당한다. 이는 1차원 배열로 변환하는 과정에서 재조정과 렌더링 최적화를 위함이다.

2-2. 전달된 컴포넌트 props을 여러 곳에 넣기

컴포넌트의 여러 개의 "구멍"에 리액트 엘리먼트들을 넣고자 할 때는, children 대신 다음과 같은 방식을 사용할 수 있다.

// 부모 컴포넌트
const SplitPane = (props) => {
  return (
    <>
      <div className="SplitPane-left">{props.left}</div>
      <div className="SplitPane-right">{props.right}</div>
    </>
  );
};
// 사용
<SplitPane left={<Contacts />} right={<Chat />} />;

children과는 달리 자식 엘리먼트를 감싸는 것이 아니라, 각자 정의한 고유한 prop에 리액트 엘리먼트를 직접 전달한다. 이게 가능한 이유는 리액트 엘리먼트는 결국 단지 객체일 뿐이기 때문이다.

3. 의존성 역전 원칙(Dependency Inversion Principle)

지금까지 살펴본 합성의존성 역전 원칙이라고 설명할 수 있다.

구조가 a->b->c 로 이뤄진 컴포넌트가 있을 때, a가 c에게 prop을 전달하기 위해 순차적으로 b로 드릴링하게 된다.
이것이 props drilling이다.

그런데 이러한 구조를 c가 a에 선언되도록 하고 b가 props로 c를 받도록 하면, 관계를 a->c->b로 역전시킬 수 있다.
이렇게 하면, 기존에 드릴링됐던 b 컴포넌트는 a와 c 컴포넌트 간 props 이동에 관여하지 않게 되어 드릴링에서 해방된다.
이를 의존성 역전 원칙이라고 설명한다.

참조
https://ko.reactjs.org/docs/context.html#when-to-use-context
https://ko.reactjs.org/docs/composition-vs-inheritance.html#containment
https://fe-developers.kakaoent.com/2021/211022-react-children-tip/
https://mxstbr.blog/2017/02/react-children-deepdive/

1개의 댓글

comment-user-thumbnail
3일 전

앗 ㅋㅋ DIP 공부하러 왔는데 마르코 글이네요~ 잘 보고갑니다~

답글 달기