리액트 children 타입 검증에 대해 (Compound Components pattern)

fethpiao·2023년 12월 18일
0
post-custom-banner

최근 구직 면접 중 리액트의 composition pattern에 관한 얘기를 나누게 되었습니다.

선호하시는 리액트 패턴이 있으실까요?

이에 주저하지 않고 Compound Components 패턴을 가장 선호한다고 답했습니다. 이 패턴은 다음과 같은 장점이 있습니다.

장점

  • prop drilling을 피하고, 각 컴포넌트의 관심사를 분리할 수 있다.
    : 자식에게 필요한 props를 부모가 직접 받는 대신, 해당 컴포넌트에게 필요한 props만을 전달해줄 수 있습니다.
// ex
<Modal 
	isVisible={isVisible} 
	setIsVisible={setIsVisible}
>
      <Modal.Header>헤더</Modal.Header>
      <Modal.Body>blablabla</Modal.Body>
</Modal>

위 코드를 만약 일반적인 방법으로 구현했다면

// ex
<Modal 
	isVisible={isVisible}
	setIsVisible={setIsVisible}
	header={'헤더'}
	content={'blablabla'}
/>

와 같은 형태가 되어 props으로 모든 데이터를 넘겨줘야하고,
무엇보다도 부모 컴포넌트 내에서 자식 컴포넌트에게 데이터를 넘겨주며 렌더링 해주어야 합니다.
이후 자식컴포넌트 props를 수정하게 될 경우 부모컴포넌트의 수정 또한 불가피해지게 됩니다.

  • 마크업 구조가 자유로워진다.
    : 부모가 props를 전달받아 자식을 직접 렌더링하는 대신 리액트의 특수한 props인 children을 이용하여 렌더링하므로, 자식 컴포넌트를 자유롭게 전달할 수 있다. (순서 변경 및 제거 및 추가가 자유롭다)
// ex
<Modal isVisible={isVisible}>
	<Modal.Header>헤더</Modal.Header>
	<Modal.Body>blablabla</Modal.Body>
	<Modal.Footer>blablabla</Modal.Body>
</Modal>

// or

<Modal isVisible={isVisible}>
	<Modal.Body>blablabla</Modal.Body>
</Modal>

단점

마크업 구조가 자유로워 진다는 것이 이 패턴의 장점이자 아쉬운 점입니다. 자유롭기에 아래와 같이 의도치 않은 코드가 작동할 수 있습니다.


// 스타일이나 로직상 필요한 wrapper 컴포넌트 없이 사용하기
<ModalHeader>헤더</Modal.Header>

// 자식 컴포넌트의 순서가 중요한 경우에도 무시하기
<Modal isVisible={isVisible}>
    <Modal.Footer>blablabla</Modal.Body>
 	<Modal.Body>blablabla</Modal.Body>
	<Modal.Header>헤더</Modal.Header>
</Modal>

// 자식 안넘겨주기
<Modal isVisible={isVisible}>
</Modal>

// 의도하지 않은 자식 내려주기
<Modal isVisible={isVisible}>
  ㅇㅅㅇ
</Modal>

이와 같은 단점들을 코드선에서 해결하지 못한다면, 개발자들간의 규칙을 숙지하는 것에 의존하게 되는 아쉬움이 생기고, 실수로 이어지기 쉽습니다.

방안들

wrapper 컴포넌트 없이 사용 => 한 파일 내에서 작성하기

아쉬운 방법이지만 부모와 자식 컴포넌트 코드의 합이 길지 않을 때 제법 유용한 방법입니다.
export에 제한을 두는 방법으로, 자바의 private, protected 클래스처럼 접근 범위를 지정할 수 없을까 하는 생각에 기인해본 방법입니다.

const ModalHeader = () => {}

const ModalBody = () => {}

const Modal = () => {}

Modal.Header = ModalHeader
Modal.Body = ModalBody

export default Modal

// 사용예시
<Modal>
  // Unresolved component ModalHeader 
  <ModalHeader></ModalHeader> 
  <Modal.Header></Modal.Header>
</Modal>

자식 컴포넌트를 별도로 export 하지 않았으므로, 부모의 instance로써 자식에 접근하지 않는 이상 자식 컴포넌트에 접근할 수 없게 하였습니다. 하지만 이는 한 파일 안에서 모든 컴포넌트를 작성해야 하고, 코드 길이는 별개로 치더라도 여러 수정사항이 한 파일에 지속적으로 영향을 준다는 단점이 있습니다.
(범위가 제한된 export가 가능했으면 좋겠습니다..)

wrapper 컴포넌트 없이 사용 => 부모

자식 컴포넌트 타입 => 타입스크립트 children type

타입스크립트를 사용하고 있다면, props의 children에 primitive 타입, 혹은 아래와 같은 타입을 지정하므로써 이를 어느정도 막을 수 있습니다.

  • ReactNode
type Props = {
  children?: ReactNode
}

가장 편리한 방법으로써 리액트 엘리먼트, primitive 타입, 프레그먼트등을 넘겨줄 수 있습니다. (가장 유연하여 편리하지만 타입 제한의 기능으로는 아쉽습니다)

  • JSX.Element
type Props = {
  children?: JSX.Element | JSX.Element[]
}

JSX.Element는 primitive 타입을 제외하고 하나의 리액트 엘리먼트를 나타내는 타입입니다. 다수의 리액트 엘리먼트를 허용하고 싶으면 위와같이 JSX.Element[] 타입을 추가하면 됩니다.

children type의 아쉬운 점

가장 바라는 점은 사실 다음과 같았습니다.

type ModalProps = {
  children?: typeof ModalHeader | typeof ModalBoxy ...
}

// ...
return (
 <Modal>
  	<Alert> // throw Error Alert는 children 타입이 아닙니다.
  		...
  	</Alert>
 </Modal>
)

위와 children 타입으로 특정 자식 컴포넌트들로 제한할 수 있다면 컴파운드 패턴의 아쉬운 점들을 해결할 수 있을 것 같습니다. 타입스크립트가 이를 지원해준다면 컴파운드 패턴의 자유성에서 비롯한 오사용 케이스들을 잡아주면서 더욱 매력적인 패턴이 될 것 같습니다. 하지만 오래전부터 이러한 아쉬움에 대한 요구가 있어왔지만 아직까지 진행된 바 없습니다.

관련글-children의 컴포넌트 타입체크 요구

필자 제안. children component 타입 체크하기(javascript, React.Children 내 함수 이용하기)

typescript 단에서 컴포넌트 타입 체크가 되지 않으므로, 자식컴포넌트 타입에 대한 엄격한 제한이 필요하다면 React.Children의 함수들로 다음과 같이 작업할 수 있겠습니다.

import {Children, ReactNode} from "react";
import ChildComponent1 from './ChildComponent1'

type Props = {
	children: ReactNode
}

const ParentComponents = ({children}: Props) => {
  // 리액트 내장함수로써 children을 순회하는 함수
  Children.forEach((children), (child) => {
    if (child.type !== ChildComponent1) {
    	throw Error('허용된 children 타입이 아닙니다')
    }
    })
// ...
	return (
      <div>
      	{children}
      </div>
    )
}


위 코드로 자식컴포넌트 타입을 체크할 수 있었습니다. typescript가 아니기에 정적 타이핑으로는 불가하지만, 데브 환경 제약을 두고 비교 로직을 수행하는 식으로 작업하면 제법 쓸만할 것 같습니다.
위 예시 외에도 React.Children내 함수들을 사용하여 자식들의 갯수를 제한하거나, 순서를 체크하거나, 특정 자식 컴포넌트는 무조건 있어야 한다는 식의 코들를 작성하는 식으로써도 활용할 수 있겠습니다.

마무리

이상으로 저의 최애 리액트 패턴인 Compound 패턴의 장단점에 대해 살펴보고 개인적인 해결법을 적어보았습니다. typescript와 javascript의 발전으로 저의 우회법이 아닌 공식적인 타입 체크가 가능한 날이 왔으면 하는 바람입니다. 또 다른 해결법을 가지고 계시다면 댓글로 남겨주시면 감사하겠습니다. 긴 글 읽어주셔서 감사합니다.

(잡담)면접은 떨어졌습니다ㅎ;

참조글

https://github.com/microsoft/TypeScript/issues/21699?ref=arahansen.com
https://www.carlrippon.com/react-children-with-typescript/
https://react.dev/reference/react/Children

profile
웹프로그래머
post-custom-banner

0개의 댓글