합성 컴포넌트 모달에 적용해보기! (feat. 클린코드)

imzzuu·2023년 3월 19일
5

몇주 전 회사에서 공통적으로 쓰이는 하단 모달 컴포넌트를 사용해서
새롭게 추가되는 페이지의 모달로 활용하고자 하던 중에 비슷하지만, 묘하게 다른 UI를 구현하려다보니 수정사항이 너무나 많아져 고민 끝에 사수님께 질문을 했고,

사수님이 합성 컴포넌트에 관한 글을 추천해주셨고, 함께 (라고 쓰고 사수님의 화려한 코드라이브를 지켜보며) 리팩토링했던 경험이 있었다.

그런데 마침 이번 주 클린코드 스터디에서 제어 역전과 의존성 주입에 대해 공부하던 중에 합성 컴포넌트가 제어 역전 패턴을 적용한 예가 될 수 있다는 것을 알게 됐다.

이 것은 공부하라는 신의 계시다! 하고 바로 정리해보려한다.

합성 컴포넌트란? (Compound Component)

컴포넌트를 합성한 패턴으로 Props Drilling 해소하고, 유연한 UI 구조를 갖는 것이 특징이다.
조금 더 구체적인 예를 통해 설명하자면,
5~6개의 기능을 하는 1개의 컴포넌트를 사용하기보다
1개의 기능을 하는 5~6개의 컴포넌트를 합성하여 사용하는 느낌이랄까...?ㅎ

수정한 모달 컴포넌트를 보면 더 와닿을 수 있을 것 같다.

이전 코드

const Page = () => {
	return (
		 <Modal>
				....
		 <Modal/>
	)
}

        
const Modal = ({Children, isOpen} : Props) =>{
	return (
	<Portal>
      <Container className={cx({ ['onAnimation']: isOpen })}>
		     {Children}
      </Container>
    </Portal>
	)
}

Modal 을 사용하는 Page 컴포넌트에서 Children으로 UI 코드를 넘겨주는 형식

합성 컴포넌트 변경 코드

const Component = () => {
	return (
		<Modal>
			<Modal.Header />
			<Modal.Body />
			<Modal.Bottom />
		<Modal/>
	)
}

이런 방식으로 header, body, bottom이 하는 일을 모두 담고 있는 Modal 컴포넌트에서
각각 header, body, bottom의 역할을 나누어 합성Modal 컴포넌트로 재탄생 하였다.

이렇게 합성 컴포넌트의 형태로 작성함으로써 어떠한 장점이 있는지 알아보자!

합성 컴포넌트 사용의 장점

1. Props 사용에서의 이점

만약 이전 코드처럼 작성했을 때,
기존의 UI에서 text 형식의 title이 아닌 이미지 형식도 들어가게 된다면?
혹은, 기존 UI의 body 부분에 UI가 check box, radio, input 형식 등이 계속해서 추가될 예정이라면?
그 때마다 Modal 컴포넌트는 그 것에 대응하기 위해 수많은 Props를 받게 될 것이고, 코드는 난잡해질 것이다.

이전 코드

const Component = () => {
	return (
		 <Modal headerText={"섭취량"} onClose={onClose} 
           		handleConfirm={handleConfirm} 
			    bottomPosition={"center"} ....>
				....
		 <Modal/>
	)

다양한 UI에 따라서 하나의 Modal 컴포넌트에 많아지는 props들...

합성 컴포넌트 변경 코드

const Componenet = () => {
...
  return (
    <Modal>
			<Modal.Header onClose={onClose}/>
			<Modal.Body />
			<Modal.Bottom handleConfirm={handleConfirm}/>
		<Modal/>
  )
}

이 처럼 자식이 필요로 하는 props를 부모를 거쳐서 전달하는 것이 아닌, 자식에게 직접 전달하면서 props drilling도 해소하면서 가독성 또한 높아진다.

2. 유연한 UI 구조

사용하는 곳에서 UI 구조를 유동적으로 바꿀 수 있다.
위에서 말했듯이 body contents의 variation은 무궁무진한 컴포넌트라면, 아래와 같이 별도의 상세 컴포넌트 (header, body, bottom 등)을 만들어 필요한 것만 가져다 갈아 끼기만 하면 된다!

// 구현부 - Modal.tsx 
import ModalHeader from "./ModalHeader.tsx"
import ModalBody from "./ModalBody.tsx"
import ModalBottom from "./ModalBottom.tsx"

const Modal = ({ children, isOpen }) => {
	...
    <Portal>
      <Background/>
      <Container className={cx({ ['onAnimation']: isOpen })}>
        <Header>
          {ModalHeader && ModalHeader}
        </Header>
        {modalBody && modalBody}
	    {ModalBottom && ModalBottom}
      </Container>
    </Portal>
}

export default Object.assign(Modal, {
  Header: ModalHeader,
  Body: ModalBody,
  Bottom: ModalBottom
});

// 사용부 
const Componenet = () => {
...
  return (
    <Modal>
		<Modal.Header onClose={onClose}/>
		<Modal.Body />
		<Modal.Bottom handleConfirm={handleConfirm}/>
	<Modal/>
  )
}

현재 이 예제는 header, body, bottom 뿐이지만, 더욱 세분화하여, checkboxBody, InputBody, RadioBody 등을 만들어서,
필요한 상세 컴포넌트만 조합하여 사용하면 좋을 것 같다.
자세한 예시와 사용법은 카카오 기술 블로그 를 참고하면 좋을 것 같다.

3. 관심사 분리

클린 코드에서 좋은 시스템 설계를 위해선 관심사를 분리하라는 구절이 나온다. 관심사란 어떠한 시스템 내에서 특정 기능, 책임, 역할, 목적 등을 담당하는 것을 의미한다.
예를 들어 주문 처리 시스템에서는 주문 관리, 재고 관리, 결제 처리 등의 관심사가 존재할 수 있다.

즉, 핵심이 되는 비지니스 로직(버튼 개수나 위치, 버튼의 배열, 일러스트의 위치 등등..)은 부모 컴포넌트에게만 가지고 있게 되며,
자식에 해당하는 상세 컴포넌트들은 그 컴포넌트의 관심사,역할에만 집중하게된다.
이런 구조일수록 의존성이 낮아져 다른 컴포넌트에서도 재사용될 확률이 높아진다.

4. 가독성

물론 합성 컴포넌트의 단점으로 코드가 길어진다는 것이 있지만,
Modal 컴포넌트 하나만 있을 때 보다,
Modal.Header, Modal.Body, Modal.Bottom 으로 나누어져 있어서 코드만 보고도 구조와 역할을 예상하기 쉽다.

급하게 결론...

이러한 장점을 갖고 있는 합성 컴포넌트의 패턴을 잘 보면, 제어 역전과 의존성 주입의 개념이 깃들어져 있다고 한다.

매주 진행하던 클린코드 스터디를 하며, 이 내용들을 어떻게 프론트에 접목시켜서 이해를 해야할지 막막했던 나에게 좋은 예가 생긴 것 같다.

이 참에 추상화, 제어 역전, 의존성 주입 등에 대해 제대로 알아봐야겠다고 생각했다.


참고 작성 출처
합성 컴포넌트로 재사용성 극대화하기
합성 컴포넌트와 render prop
단단한 컴포넌트 부수기

profile
FrontenDREAMER

0개의 댓글