목차
기존 큐샵의 공통 컴포넌트는 별도의 디자인시스템 없이 mui 기반으로 하여 필요한 조건들을 props 로 전달했다.
하지만 프로젝트의 규모와 작업 인원이 늘어나면서 컴포넌트의 수정 빈도가 증가하고, 이에 따른 관리의 어려움이 있었다.
“A님! 여기에 footerCustomStyle props 추가하셨는데 기존 스타일이 변경된건가요?”
“B님! disabledCheck props 는 기존의 useCheck을 false 값으로 전달하면 되지 않을까요?”
추가적으로 생겨나는 props 를 막기 위해 필사적으로 코드리뷰를 했지만 요구사항대로 작업하려면 어쩔수 없는 부분도 생겨났다. 한마디로 지저분한 코드가 된것이다.
마침 ✨ 새로운 디자이너분✨ 이 합류하여 큐샵의 새로운 디자인 시스템을 도입하기로 했다.
이때다 싶어 카카오 FE 기술 블로그에서 봤던 “합성컴포넌트로 재사용성 극대화 하기” 라는 글을 팀원들에게 소개하며 우리도 합성 컴포넌트 형태로 개발 해보자고 제안했다.
1. 합성 컴포넌트 (Compound component) 란?
개념
합성 컴포넌트는 컴포넌트를 조립하거나 구성 요소를 재사용할 수 있도록 설계하는 개발 방식이다.
여러 개의 작은 컴포넌트를 조합하여 하나의 큰 컴포넌트를 만드는 방식으로, Sub Components 가 Main Component 내부의 상태를 공유하는 패턴으로 이루어진다.
각 Sub components 들은 독립적으로 큰 의미가 없지만, 이를 조합하여 화면에 그려진다면 의미있는 요소가 된다.
html 요소들 중에서도 <select><option> , <details><summary>, <table><tr><th> 등 존재한다.
<table>
을 예시로 들어보겠다.하위 <tr>,<th>,<td> 들의 조합으로 구성되어 하나의 의미있는 테이블 UI 를 그려내지만, 독립적으로 사용 시 의미가 없어진다.
장점
- 높은 재사용성
컴포넌트를 독립적이고 유연하게 설계할 수 있어 다양한 요구사항에 맞게 조합할 수 있다.
ex : 버튼에 아이콘, 텍스트, 로딩 스피너를 조합하여 상황에 맞는 버튼 생성
- 유지보수 용이성
코드를 더 작고 읽기 쉽게 작성할 수 있어 수정 및 확장이 용이하다.
ex : 여러 종류의 모달을 하나의 모달 컴포넌트로 통합
- 의존성 최소화
하위 컴포넌트는 독립적으로 동작하기 때문에 다른 컴포넌트의 수정에 영향을 덜 받는다.
- 개발 생산성 향상
공통 컴포넌트를 기반으로 다양한 기능을 추가하기 쉽기 때문에 개발 속도가 빨라진다.
단점
- 초기 설계가 어렵다
어떤 컴포넌트를 합성할지, 어디까지 분리할지 판단하기 어렵다. 초기 설계가 잘못되면 이후 수정이 더 복잡해질 수 있다.
- 러닝커브
팀원 모두가 합성 컴포넌트 패턴에 익숙하지 않다면 이해와 활용에 시간이 필요하다.
- 의외의 귀찮음?
실제로 사용하면 가장 기본적인 형태의 컴포넌트도 일일히 작성해야 하는 번거로움이 있을 수 있다.
props 방식이 더 편리한 경우도 있으니 컴포넌트 요구사항에 따라 적절하게 사용하는것이 좋겠다.
2. 기존 코드 💩
내가 가장 먼저 합성 컴포넌트를 시도했던 “툴팁”을 예를 들어 보겠다.
props 방식으로 작성했던 기존의 코드는 다음과 같다.
놀랍게도 처음에는 타이틀, 컨텐츠, 닫기 버튼, 사이즈 를 위한 몇 가지 props만으로 간단히 설계된 툴팁이었다.
하지만 여러 작업자가 거치면서 복잡한 문제들이 생겨났다.
첫째로, 스타일을 수정하고 싶었던 누군가 {…props} 를 추가하며 컴포넌트 내부에서 조건부 처리로 구현했다.
이로 인해 원래 정의된 size props는 사실상 무용지물이 되었지만, 이미 여러 곳에서 사용되고 있어 삭제할 수도 없는 상황이 됐다.
두번째로, 툴팁의 title 아래에 표시되는 설명글인 desc가 이미 사용 중인 상황에서, 동일한 디자인의 설명글을 title 위에 표시해야 하는 요구사항이 생겼다.
아마 당시의 나는 많은 고민을 거쳐 eyebrow라는 새로운 props를 추가하고, desc 스타일을 상속하여 처리했던 것 같다.
이 두가지 예시만 보더라도, 각자 사정에 따라 기능들이 추가되고 이런 히스토리가 쌓이게 되면 유지보수가 힘들 뿐만 아니라 이미 사용하고 있는 곳에서 사이드이펙트가 발생할 가능성이 크다.
툴팁뿐만 아니라 팝업, 탭, 인풋 등 다른 공통 컴포넌트들도 비슷한 방식으로 점차 복잡해지고 있었다.
각 컴포넌트가 다양한 요구사항에 맞춰 수정되고 확장되면서, 재사용성이 높은 컴포넌트라고 부르기는 어려웠다.
합성 컴포넌트를 적극적으로 활용한다면 단순한 코드 작성 패턴을 넘어, React의 장점을 극대화하는 중요한 접근 방식이 아닐까 생각이 들었다.
3. 합성 컴포넌트로 코드 작성하기
섹션분리
디자이너분께서 툴팁의 여러 케이스를 잡아주셔서 합성컴포넌트 규격화를 위해 섹션을 먼저 잡았다.
물음표
- 생소한 용어일때 사용
- 요소 : 타이틀, 컨텐츠(길이제한 없음), 텍스트버튼, 닫기버튼
느낌표
- 이미 아는 용어지만, 부연 설명이 필요한 경우 사용
- 요소 : 컨텐츠(최대 4줄), 텍스트버튼, 닫기버튼
서브 컴포넌트 구현
툴팁을 구성하는 컴포넌트들로, 독립적으로 의미를 가지고 있지 않는 부분을 의미한다.
위에서 예시로 들었던 <table> 요소의 <th><td>에 해당한다고 볼 수 있다.
대부분의 라이브러리에서 툴팁 컴포넌트 내부에 버튼을 구현하고, label props 로 툴팁 컨텐츠를 표기하는 방식이 많았다.
나같은 경우, 라벨 부분에 많은 커스텀이 들어가고 복잡성을 띄기 때문에 타이틀, 컨텐츠, 링크가 들어가는 영역인 푸터 총 세개의 영역을 서브 컴포넌트로 나누었다.
(다행히도 디자이너 분께서 툴팁의 설명글은 빼주셨다.)
추가적으로, 커스텀 가능한 버튼을 구현했다.
초기 디자인은 물음표와 느낌표 두가지 버튼으로 구성되어 있지만 확장성을 고려하여 추가했다.
커스텀 툴팁버튼의 경우, 말그대로 clickable 한 어떠한 형태의 엘리먼트가 오더라도 액션을 취할 수 있어야 하기에 ref 값과 onClick 이벤트만 전달할 수 있도록 구현했다.
툴팁의 이벤트처리가 포함된 Root 컴포넌트를 생성하여 서브컴포넌트를 한번 더 감싸는 Wrapper 처리했다.
이 Root 컴포넌트는 엄밀히 말하면 Main 컴포넌트는 아니고, 툴팁에서 복잡하게 사용되는 로직 처리에 대한 context 를 내려주는 역할인 것이다.
메인컴포넌트 구현
이제 서브컴포넌트의 부모격인, 메인 컴포넌트를 구현한다.
이 메인 컴포넌트에 서브 컴포넌트들을 묶어 children을 통해 화면에 실제로 렌더링 되도록하는 역할을 한다.
이때, 커스텀 버튼을 사용할 경우와 사용하지 않는 경우(기본 아이콘) 로 한번 더 나누었다.
자주 사용될 Tooltip 버튼 아이콘까지 합성 컴포넌트로 작성하게 하는것은 오히려 작업자들이 귀찮게 느껴질 것 같아 type 으로 내려받도록 했다.
아니나 다를까, 추후 텍스트가 길어 ellipsis 처리가 필요한 경우 추가적인 툴팁버튼 구현이 필요했다.
평소같았으면 별도의 props 를 추가했겠지만, 이 경우 합성컴포넌트로 적절하게 대처하여 개발할 수 있었다.
메인 & 서브컴포넌트 export
구현된 모든 서브 컴포넌트들을 묶어 export 해주면 된다.
툴팁은 다음과 같이 잘게 쪼갠 컴포넌트로써 사용될 수 있다.
4. 최종 코드
코드리뷰 시간에 기본적인 형태를 나타낼 때도 조립하는 것이 번거롭다는 몇몇 팀원들의 의견을 반영하여 “가장 기본적인 형태” 는 props 로 전달받기로 했다.
즉, 두개의 형태가 있는 것이다.
TooltipBox 컴포넌트를 다음과 같이 수정했다.
props 로 받는것 자체가 가장 기본적인 형태를 위한것이므로 위치도 정해져야 한다고 생각했다.
타이틀, 컨텐츠 순으로 그려주고 합성컴포넌트를 위한 children 요소를 위치시켰다.
link 는 어떠한 경우든 맨 마지막에 나와야 하므로 맨 마지막에 위치했다.
최종적으로, 두가지 형태의 툴팁은 다음과 같이 사용 되어졌다.
- props version
- 합성 컴포넌트 version
5. 결론
이번에 Tooltip에 합성 컴포넌트를 적용하면서 장, 단점을 여실히 느낄 수 있었다.
컴포넌트의 확장성을 높이고 재사용성을 높여 다양한 디자인 요구사항에 빠르게 대응할 수 있기 때문에 개인적으로 만족스러운 결과물이었다.
하지만 작업자에 따라 그리고 구현 기능 범위에 따라 적절하게 사용 되어야 하기 때문에 항상 정답이 될 수는 없다.
가장 기본적인 요구사항에서는 props 방식이 더 직관적이고 간편하겠지만,
요구사항이 복잡하고 조금 더 다양한 상황을 고려해야 한다면, 합성 컴포넌트 패턴은 재사용성을 만족시키고 사이드이펙트를 줄이는 면에서 훌륭한 대안이 될 수 있다.
단일 책임 원칙(SRP)을 지키며 각 컴포넌트가 명확한 역할을 갖도록 설계하면, 코드의 유지보수성과 확장성을 동시에 확보할 수 있다.
결국, 상황에 맞는 적절한 선택과 설계가 가장 중요한 부분임을 다시 한번 깨닫게 되었다.
참조
🔗 Mantine
🔗 카카오 FE 기술블로그