[10분 테코톡] 합성 컴포넌트 패턴

흑우·2023년 11월 28일

10분 테코톡 - 5기

목록 보기
1/16

컴포넌트 합성

컴포넌트 합성이란?

컴포넌트에서 다른 컴포넌트 담기

  • children을 통해 컴포넌트 주입
const App = () => {
	<Parent>
    	<Child />  
    <Parent>
}
  • props를 통해 컴포넌트 주입

const App = () => {
	<SplitPane
  		left={
        	<Contacts />
        }
  		right={
        	<Chat />
        }
  	/>
}
        
const SplitPane = (props) => (
	<section>
  		<div>
  			{props.left}
  		</div>
  		<div>
  			{props.right}
  		</div>
  	<section>
  
)

합성을 통해서 얻고자 하는 것은 무엇일까?

합성을 사용하여 컴포넌트 간에 코드를 재사용하는 것이 좋습니다.

합성의 장점 - 재사용성

const AwesomeButton = () => (
	<button>awesome</burron>
)

const AwesomeUI = () => {
	<Layout>
    	<AwesomeButton />  
    </Layout>
}

SRP (Single Responsibility Principle)

  • 단일 책임 원칙 => 관심사의 분리
  • 데이터 패칭, 에러 핸들링, 로딩에 대한 처리가 하나의 컴포넌트에서 일어난다.
const AwesomeConponent = () => {
	const { data, isLoading, isError } = useAwesomeFetch();
  
  	if(isError) return <div>error...!<div
  	if(isLoading) return <div>loading...!<div>;
  	return <div>{data}<div>;
}
  • 관심사를 분리해보자! (ErrorBoundary와 Suspense 사용!)
const AwesomeUI = () => {
	<ErrorBoundary fallback={<div>error...!</div>}>
      <Suspense fallback={<div>loading...!</div>}>
      	<AwesomeComponent />                   
      </Suspense>
    </ErrorBoundary>                     
}

합성 컴포넌트 패턴

  • 이러한 컴포넌트 합성의 장점을 활용하기 위해 고안된 패턴
  • 하나의 컴포넌트를 여러 가지 집합체로 분리한 뒤
  • 분리된 각 컴포넌트를 사용하는 쪽에서 조합해 사용하는 방식
  • 합성 컴포넌트의 대표적인 예시는 select 태그!
<select>
  <option value="value1">value1</option>
  <option value="value2">value2</option>
  <option value="value3">value3</option>
</select>
  • select 컴포넌트는 변경에대한 관심사를 option은 벨류에 대한 관심사를 가지고 있습니다.
  • option과 같은 컴포넌트를 서브 컴포넌트, select와 같은 컴포넌트를 메인 컴포넌트로 표현하겠습니다.

합성 컴포넌트 만들기

  • 모달을 호출하는 버튼의 디자인이 변경되면 어떻게 하죠?
  • 모달의 이름이 변경되면 어떻게 하죠?
const ReviewFilterModal = () => {
	const [isOpen, openModal, closeModal] = useModal();
  
  	return (
    	<>
      		<AwesomeButton onClick={openModal}>필터<AwesomeButton>
      		{isOpen && (
             	<Portal>
             		<Backdrop onClick={closeModal} />
      				<AwesomeModalBox>
      					<Header>
      						<div>리뷰 검색 필터<div>
      						<Close>x<Close>
      					<Header>
                        <ModalContent>
      				<AwesomeModalBox>
             	</Portal>
             )}
      	<>
    )
}
  • 변경이 가능한 부분을 props로 내려 받는 걸로 변경했어요
const Modal = ({ trigger, title }) => {
	const [isOpen, openModal, closeModal] = useModal();
  
  	return (
    	<>
      		{trigger}
      		{isOpen && (
             	<Portal>
             		<Backdrop onClick={closeModal} />
      				<AwesomeModalBox>
      					<Header>
      						{title}
      						<Close>x<Close>
      					<Header>
                        <ModalContent>
      				<AwesomeModalBox>
             	</Portal>
             )}
      	<>
    )
}
  • 닫기 버튼을 왼쪽으로 옮겨 주세요!
  • 여기는 백드랍을 제거해 주세요!
  • 닫기 아이콘을 하트로 변경해 주세요!
  • 다양한 요구사항이 있을 수 있고 모두 props로 처리할 수 있겠죠?
  • 하지만 props가 늘어날 수록 구현부가 복잡해진다.
  • 이러한 문제를 합성 컴포넌트로 해결해보자!
const ModalTrigger = ({ children, style }) => (
	<button style={style || defaultTriggerStyle}>{children}<button>
)

const ModalBackdrop = ({ style }) => (
	<div aria-hidden style={style || defaultBackdropStyle}><div>
)

const ModalClose = ({ style }) => (
	<button style={style || defaultCloseStyle}><button>
)

const ModalContent = ({ style }) => (
	<div style={style || defaultContentStyle}><div>
)

const Modal = ({ children }) => children
  • 이렇게 사용할 거 같네요
<Modal>
  <ModalTrigger />
  <ModalBackdrop />
  <ModalContent>
  	<ModalClose />
  <ModalContent>
<Modal>
  • 리뷰 검색 필터에 적용해보면?
<Modal>
  <ModalTrigger>필터<ModalTrigger>
  <Portal>
  <ModalBackdrop />
  <ModalContent>
  	<Flex>
  	<Title>리뷰 검색 필터</Title>
  	<ModalClose>X<ModalClose>
    <Flex>
    <FilterSection>
  <ModalContent>
  <Portal>
<Modal>
  • 하지만 이 상태에서는 상태가 없기 때문에 모달과 상호작용을 할 수 없죠.
  • 상태를 조작하는 컴포넌트 => ModalTrigger / ModalClose / ModalBackdrop
  • 상태에 영향을 받는 컴포넌트 => ModalContent / ModalBackdrop
  • 이러한 컴포넌트들은 독립적인 컴포넌트이기 때문에 상태를 공유하기가 어렵습니다.
  • ContextAPI로 컴포넌트 내부 데이터를 공유해보자
const Modal = ({ children }) => {
	const [isOpen, openModal, closeModal] = useModal();
  
  	return (
    	<ModalProvider value={{ isOpen, openModal, closeModal }}>
      		{children}
      	<ModalProvider>
    )
}
  • 이렇게 적용하면 정상적으로 모달과 상호작용할 수 있습니다.
const ModalTrigger = ({ children, style }) => {
  	const { openModal } = useModalContext();
  	return (
    	<button style={style || defaultTriggerStyle} onClick={openModal}>{children}<button>
    )
}

const ModalContent = ({ style }) => (
  const { isOpen } = useModalContext();

  return isOpen ? (<div style={style || defaultContentStyle}><div>): null	
)
  • 하지만 여전히 가독성이 아쉬운데요.
  • 합성 컴포넌트 패턴은 메인 컴포넌트에 서브 컴포넌트를 속성으로 추가함으로써 문제를 해결합니다.
const Modal = ({ children }) => {
	const [isOpen, openModal, closeModal] = useModal();
  
  	return (
    	<ModalProvider value={{ isOpen, openModal, closeModal }}>
      		{children}
      	<ModalProvider>
    )
}

Modal.Trigger = ModalTrigger
Modal.Backdrop = ModalBackdrop
Modal.Close = ModalClose
Modal.Content = ModalContent
  • 이렇게 추가된 컴포넌트는 같은 관심사인지 한 눈에 확인 가능합니다.
<Modal>
  <Modal.Trigger>필터<Modal.Trigger>
  <Portal>
  <Modal.Backdrop />
  <Modal.Content>
  	<Flex>
  	<Title>리뷰 검색 필터</Title>
  	<Modal.Close>X<Modal.Close>
    <Flex>
    <FilterSection>
  <Modal.Content>
  <Portal>
<Modal>

합성 컴포넌트의 장단점

장점

  • 변경에 유연한 UI
  • 더 적은 prop drilling
  • 가독성

단점

  • 코드의 길이가 늘어남
  • 구현 복잡도 => 사용자에게 더 많은 제어권과 분기를 제공함
  • 만약에 상태를 조건적으로 조작하고 싶다면?
  • 기존의 코드에서는 상태가 Modal 컴포넌트 내부에 있기 때문에 해당 요구사항에 대응할 수 없습니다.
  • Close 버튼은 전달 받은 작동만 진행하기 때문에 외부 조건에 대응할 수 없습니다.
const Modal = ({ children }) => {
	const [isOpen, openModal, closeModal] = useModal();
  
  	return (
    	<ModalProvider value={{ isOpen, openModal, closeModal }}>
      		{children}
      	<ModalProvider>
    )
}
  • 이 문제에 대응하기 위해서는 외부에서도 상태를 주입받을 수 있도록 구조를 변경해줘야 합니다.
const ReviewFilterModal = () => {
  	const [isOpen, setOpen] = useModal(false)
    
    const safetyClose = () => {
    	setOpen(confirm('닫으면 필터가 적용되지 않습니다. 닫으시겠습니까?'))
    }
  	
	<Modal open={isOpen} onOpenChange={setOpen}>
  	...기존 코드
    	<Modal.Close onClick={safetyClose}>X<Modal.Close>
	<Modal>

}
// open props를 통해 최종적으로 사용될 상태 결정        
const Modal = ({ children, open }) => {
	const [isOpen, openModal, closeModal] = useModal();
  
  	const composeIsOpen = open ?? isOpen
  	return (
    	<ModalProvider value={{ isOpen: composeIsOpen, openModal, closeModal }}>
      		{children}
      	<ModalProvider>
    )
}

const ModalClose = ({ children, style }) => (
  	const { closeModal } = useModalContext();

	const composedOnClick = composeFunctions(onClick, closeModal);
	<button style={style || defaultCloseStyle} onClick={composedOnClick}>{children}<button>
)
  • 사용자에게 더 많은 제어권을 줄 수록 구현 난이도는 올라갑니다.

마무리

지금까지 합성 컴포넌트 패턴에 대해서 알아봤는데요. 모든 디자인 패턴이 그렇듯 합성 컴포넌트 패턴 또한 장단점을 가지고 있고, 사용하는 이유를 명확하게 알게되었던 시간 같습니다. 단점으로 나왔던 코드의 길이가 늘어난다는 것의 개인적인 의견을 말해보자면 클린 코드는 짧은 코드가 아니라 읽기 쉬운 코드라는 말이 있는 만큼 코드의 길이보다는 가독성이 더 중요하다고 생각해 저는 현재 진행하고 있는 프로젝트에 적용해볼 예정인데요. 현재 Modal을 구현하고 있는 방법과는 살짝 달라서 저한테 맞는 방식으로 변경해서 적용해볼 예정입니다.

Reference

profile
흑우 모르는 흑우 없제~

0개의 댓글