Prop Drilling은 props를 오로지 하위 컴포넌트로 전달하는 용도로만 쓰이는 컴포넌트들을 거치면서 React Component 트리의 한 부분에서 다른 부분으로 데이터를 전달하는 과정이다.
prop drilling이 보통 3~5개 컴포넌트를 거치는 정도이면, 괜찮을 수 있으나, 10개 이상을 거친다면 중간 컴포넌트들은 불필요하게 prop을 받게 되어 가독성이 떨어져 유지보수가 어려워진다.
이를 해결하기 위해 전역 상태관리 라이브러리를 사용할 수 있으며, 간단하게는 context api를 사용할 수 있다.
리액트에서 context를 쓰는 이유는 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공
하기 위함이다.
context는 전역적(global)
으로 데이터(현재 로그인한 유저, 테마, 선호 언어 등)를 공유할 수 있는 방법이다.
리액트 공식문서에서는 context를 사용하면 컴포넌트를 재사용하기 어려워지므로 꼭 필요할 때만 쓸 것
을 권장한다.
context보다 컴포넌트 합성(composition)
이 더 간단한 해결책일 수 있다.
컴포넌트에서 다른 컴포넌트를 담는 것이 합성이다.
어떤 컴포넌트들은 어떤 자식 엘리먼트가 들어올 지 미리 예상할 수 없는 경우가 있다. 예를 들어, 모달 컴포넌트 등에서 찾아볼 수 있는데, 이러한 컴포넌트에서는 children
prop을 사용하여 자식 엘리먼트를 출력에 전달할 수 있다.
부모 컴포넌트는 자신이 감싸고 있는 자식 엘리먼트들을{props.children}
라고 적은 부분에 출력할 수 있다.
단지 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(children)
메서드는 children을 평평하게 만들어 1차원 배열(flat array)로 변환해서 리턴한다. children의 집합을 다루고 싶을 때 유용하며, 배열이 아닌 opaque data structure일 때도 동작한다.opaque data structure가 의미하는 바는, children이 어배열, 함수, 객체 등 어떤 타입이어도 괜찮다는 것이다. 특히, children을 하부로 전달하기 전에 다시 정렬하거나 일부만 잘라내고 싶을 때 유용하다.
또한, 이 메서드는 각 child 들에 고유한 key를 할당한다. 이는 1차원 배열로 변환하는 과정에서 재조정과 렌더링 최적화를 위함이다.
컴포넌트의 여러 개의 "구멍"에 리액트 엘리먼트들을 넣고자 할 때는, 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에 리액트 엘리먼트를 직접 전달한다. 이게 가능한 이유는 리액트 엘리먼트는 결국 단지 객체
일 뿐이기 때문이다.
지금까지 살펴본 합성
은 의존성 역전 원칙
이라고 설명할 수 있다.
구조가 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/
앗 ㅋㅋ DIP 공부하러 왔는데 마르코 글이네요~ 잘 보고갑니다~