공통 컴포넌트를 건강하게 기르기 위한 고민

공통 컴포넌트를 건강하게 기르기 위한 고민

시작하며

공통 컴포넌트가 이렇게 많다니!

프론트엔드 개발자 혹은 개발자라면 누구나 한 번쯤 이런 순간을 마주합니다.
“이거 공통으로 빼야할까?”
입사 4개월 차 차차와 3년 차 페페 역시 같은 질문에서 대화를 시작했습니다.



입사 4개월 차인 차차가 처음 레포지토리를 열었을 때, 가장 먼저 든 생각은 단순했습니다.


“공통 컴포넌트가 이렇게 많다니.”


이전 회사에서는 경험하지 못한 규모였고, 쓸 수 있는 재료가 많다는 점은 분명 장점처럼 느껴졌습니다.


하지만 동시에, 어디까지가 사용 가치가 있는 컴포넌트인지 알기 어려웠습니다.


chacha: 공통 컴포넌트가 많으니 어떤 기준으로 써야 할지부터 막막해지더라고요. docs가 있었다면 구조를 이해하는 데 훨씬 수월했을 것 같아요.

pepe: 지금 구조는 처음부터 이렇게 만들어진 건 아니에요. 상품이 늘어나면서, 재사용을 위해 조금씩 공통화가 진행됐고 그 과정에서 컴포넌트가 세밀해졌죠.

공통 컴포넌트의 증가는 의도된 선택의 결과였지만 동시에 문서화되지 않은 결정들이 쌓이며 이제는 그 히스토리를 모두가 공유하기 어려운 상태가 되었습니다.
이 글은 이 질문에 대한 단순한 정답을 제시하려는 글이 아닌
공통 컴포넌트가 팀 생산성을 높이다가, 언제부터 갉아먹기 시작하는지에 대한 솔직한 기록입니다.

공통 컴포넌트의 탄생

이 이야기는 차차의 고민에서부터 시작되었습니다.
개발 중 이미 구현되어있는 TextArea 대신 새로운 형태의 TextArea가 필요해졌습니다.
찾아보니, 이미 특정 도메인에서 사용되고 있는 StaticLabelTextArea를 발견했습니다. 겉보기에는 공통 컴포넌트로 분리해도 충분해 보였습니다. 또한 실제로 비슷한 형태의 입력 컴포넌트가 여러 화면에서 반복될 가능성도 있었습니다.



chacha: StaticLabelTextArea와 같은 구현체가 필요했어요. 처음에는 자연스럽게 공통 컴포넌트로 만들어야 하나 고민했어요


하지만 구현부를 자세히 들여다볼수록 몇 가지 걸리는 지점들이 있었습니다.




해당 컴포넌트는 내부적으로 deprecatd 예정인 컴포넌트에 의존하고 있었고 실제 사용처 역시 두 곳에 불과했습니다. 공통화로 얻을 수 있는 이점에 비해, 단점이 더 클 것으로 보였습니다. 향후 변경 시 감당해야 할 유지 비용이 더 커질 수 있다는 신호가 느껴졌기 때문입니다. 특히 의존하고 있는 하위 컴포넌트가 변경될 경우 그 영향 범위를 예측하기 어렵다는 점이 가장 큰 부담이었습니다.


이 시점에서 ‘공통 컴포넌트를 만들 수 있느냐’라는 고민은, ‘언제 잘 만들었다고 말할 수 있는가’ 라는 질문으로 바뀌어 관점이 확장되었습니다.



공통 컴포넌트의 사춘기

이렇게 만들어진 공통 컴포넌트들은, 시간이 지나며 예상치 못한 형태로 성장하기도 합니다.
마치 사춘기를 겪는 사람처럼, 명확한 경계 없이 역할을 계속해서 받아들이다 보면 점점 본래의 목적과는 다른 모습이 되기도 합니다. 그럼 공통 컴포넌트가 ‘사춘기’를 겪으며 우리를 곤란하게 만들었던 몇 가지 패턴을 살펴보겠습니다.



공통에 있어 역할이 늘어나는 건 정말 올바른 방향일까?


여러 사람의 손이 들어가기 시작한 공통 코드는 시간이 지날수록 더 많은 요구사항을 받게 됩니다. 초기에는 명확하고 단일한 목적을 가졌을지 몰라도 재사용되기 시작하면서 새로운 역할이 하나둘 추가되는 것처럼요.


문제는 이 확장이라는 것이 언제나.. 항상.. 올바른 성장으로 이어지지 않는다는 점입니다. 기능과 가능성이 늘어날수록 책임 범위가 흐려지곤 합니다. 기존의 목적이 무엇이었는지 설명하기 어려워졌을 때 우리는 다시 한번 코드를 돌아볼 필요가 있습니다. 기능과 가능성이 늘어날수록 책임 범위가 흐려지곤 합니다. 기존의 목적이 무엇이었는지 설명하기 어려워졌을 때 우리는 다시 한번 코드를 돌아볼 필요가 있습니다.



예를 들면 모바일 채널 내에서 사용되고 있는 컴포넌트 중 CoverageSelectCard 가 그런 사춘기를 겪고 있습니다.





CovergeSelectCard는 보험 상품 선택이라는 비교적 단순한 UI를 담당하기 위해 만들어졌지만, 시간이 지나며 다양한 요구사항을 흡수하게 되었습니다. 특히 바텀시트와 관련된 여러 동작이 컴포넌트 내부로 자연스럽게 모이기 시작했습니다.


- 바텀시트 열기/닫기 처리
- iOS 환경 이슈 대응을 위한 body height 조작
- 드래그 인터랙션 처리
- 애니메이션 및 딜레이 제어

각각의 로직은 그 시점에서 반드시 필요했던 기능이었고, 실제 문제를 해결하기 위한 선택이었습니다.

pepe: 바텀시트 문제를 해결하기 위해 로직들이 쌓이기 시작했어요. 그러면서 점점 더 많은 책임을 동시에 수행하는 구조가 되어버린 거예요. 그래서 새로운 옵션이 추가될 때마다 컴포넌트 자체를 수정해야 하는 상황도 잦아졌어요ㅠ.ㅠ

이 현상은 오랜 기간 사용된 컴포넌트에서 흔히 관찰할 수 있는 성장 과정과 닮아 있습니다.

처음에는 작고 단순한 컴포넌트로 시작 ->
요구사항이 추가될 때마다 기존 코드 위에 기능이 덧붙여짐 ->
명확한 리팩토링 시점을 잡지 못함 ->
어느새 여러 역할을 동시에 수행하는 컴포넌트가 됨

특히 보험 도메인 특성상 복잡한 비즈니스 규칙과 다양한 예외 케이스가 존재한다는 점도 이러한 비대화에 일정 부분 영향을 주었습니다.
중요한 점은 CoverageSelectCard가 오랜 시간 동안 실제 문제를 해결해 왔고, 그 과정에서 자연스럽게 많은 책임을 떠안게 되었다는 것입니다.
이 시점에서의 질문은 “이 컴포넌트가 나쁜가?”가 아니라, “이 역할들을 언제, 어떤 기준으로 나누는 것이 앞으로의 유지보수에 더 적합한가?”로 바뀌게 됩니다.



아직 독립할 준비가 되지 않은 컴포넌트들

모든 공통 컴포넌트가 역할이 과도하게 늘어나며 비대해지는 방향으로 성장하는 것은 아닙니다. 어떤 컴포넌트들은 충분히 성장하기도 전에 공통이라는 이름을 먼저 갖게 됩니다.
DelayRender는 그런 사례에 가까운 컴포넌트였습니다.





RenderAfter는 일정 시간(delayTime)이 지나기 전까지는 fallback을 보여주고, 시간이 지나면 children을 렌더링하는 컴포넌트입니다. 코드 크기도 작고, 특정 도메인에 얽혀 있지 않은 순수 UI 유틸리티 성격을 가지고 있습니다. 이런 맥락에서 RenderAfter를 공통 컴포넌트로 분리한 선택은, 당시로서는 충분히 합리적인 판단이었습니다. 다만 시간이 지나며 조금 다른 관점의 질문이 생기기 시작했습니다.


이 컴포넌트는 정말 공통으로 사용되고 있는가?

현재 DelayRender는 소수의 화면에서만 사용되고 있습니다.
이론적으로는 ‘딜레이 후 렌더링’이라는 패턴이 다양한 화면에서 필요해질 수 있지만, 반대로 앱 내부에서 처리해도 충분한 규모이기도 합니다.
이 지점에서 DelayRender는 공통 컴포넌트로 보아도 틀리지 않고 그렇다고 이상적인 공통 컴포넌트라고 말하기도 어려운 경계선 위에 놓이게 됩니다.
컴포넌트 자체는 작고 독립적이며 관리 부담도 크지 않습니다. 각 사용처 내부에 두었더라도 큰 문제가 되지는 않았을 것입니다. DelayRender는 공통 컴포넌트로서 충분히 합리적이었지만, 아직 독립했다고 말하기에는 이른 상태에 가까웠습니다. 이런 경우 중요한 것은 정답을 정하는 것이 아니라 판단 기준을 팀 내에서 공유하는 일입니다.

  • 몇 번의 반복부터 공통으로 볼 것인가
  • 실제 사용 빈도와 관리 비용을 어떻게 저울질할 것인가
  • “지금은 공통이 아니다”라고 말할 수 있는 시점은 언제인가

DelayRender는 공통 컴포넌트를 언제 만들 것인가에 대한 질문보다 언제까지 공통으로 유지할 것인가를 고민하게 만든 사례였습니다.



공통 컴포넌트의 결혼(결합)

chacha: 그렇다면 템플릿은 어떤가요? 템플릿도 굉장히 많은데, 어떤 기준에서 템플릿이 만들어지게 된건지 궁금해요!

pepe: 보험 도메인에서는 템플릿이 불가피하게 필요한 순간도 있다고 생각해요. 청약 씬을 보면 가입설계 계약체결동의, 미리보기 문서, 계약자확인사항 등… 대부분의 상품 청약 플로우에서 반드시 거쳐야 하는 영역들이 있죠!





  • 단일 컴포넌트 : 자유의 상태

    • 단일 컴포넌트는 하나의 책임만을 가집니다. 입력을 받고 정보를 보여주고 상태를 표현하는 것처럼 명확하게 본인의 역할에만 집중합니다.
  • 느슨한 조합 : 관계를 맺기 시작하는 상태

    • 여러개의 단일 컴포넌트가 조합되어 하나의 화면이나 기능이 만들어지면 이때 단일끼리 최소한의 관계가 생깁니다.
  • 템플릿 : 자유가 아닌 상태

    • 여러개의 컴포넌트를 단순히 묶은 형태가 아닌 어떠한 결정을 코드로써 고정하는 구조라고 생각합니다.


그래서 우리는 언제 결합해야 하고, 언제 분리해야 할까?

  • 결합을 선택해도 되는 그린 라이트! ✅
    • 같은 조합이 복붙되고 있다. (ex 청약씬에 존재하는 청약사항확인씬은 거의 동일)



    • 조합의 주체가 명확하다.



  • 아직 템플릿이 아니라는 레드 라이트! ❌
    • 화면마다 예외가 계속 생긴다.



    • 불확실한 미래를 걸고 뺀 경우 / 충분한 검증 기간, 케이스가 거쳐지지 않은 경우

즉, 템플릿은 재사용을 위한 도구가 아니라 변경하지 않기로 합의한 결정 집합으로 본다면 구분이 더 명확할 것으로 생각합니다.

공통 컴포넌트를 위한 추모

원피스의 명대사 중 하나로 이런 대사가 있습니다.




[사람은 언제 죽는다고 생각하나? 심장이 총알에 꿰뚫렸을 때? 천만에. 불치의 병에 걸렸을 때? 천만에. 맹독 버섯 수프를 먹었을 때? 천만에! 사람들에게 잊혀졌을 때다!

이건 비단 사람만의 이야기가 아닙니다.

chacha: 공통 컴포넌트는 언제 죽었다고 판단할 수 있을까요?

  1. 아무도 찾지 않을 때
  2. 외부 정책 요인으로 인해서도 deprecated 가능 ex) payfit 적용
  3. 로직이 너무 복잡해서 건들면 다 죽을 것 같을 때

애석하게도 어떤 컴포넌트들은 시간이 지나며 점점 아무도 손대지 않으며 유기된 존재? 어려운 존재?가 됩니다.





시간이 지나며 옛날에 만들었던 공통 컴포넌트는 재사용의 이점을 제공하기보다는 수정과 검증의 부담을 발생시키는 비용 요소가 되기도 합니다.
이 시점에서 ‘deprecated’는 실패의 선언이 아니라 비용을 절감하기 위한 선택이 됩니다.


deprecated의 시작은 사용하지 않음이 아니라 의도적으로 선택되지 않음인 경우가 많습니다.
새로운 요구사항을 만족시키지 못하거나, 더 이상 팀의 기본 선택지로 고려되지 않는 순간 공통 컴포넌트는 자연스럽게 죽음에 다다릅니다.



아마도 다수의 개발 조직들이 공통 컴포넌트를 영구 자산으로 보지 않을 것으로 추측합니다.


디자인 시스템의 진화, 조직 구조 변화, 유지 비용 증가와 같은 이유로 의도적으로 deprecated를 선택하고 대체 컴포넌트를 제시하는 경우가 많습니다.






리엑트 디자인 시스템을 deprecated 후 고통받는 폴라리스…


출처 : https://community.shopify.dev/t/is-anyone-else-disappointed-with-polaris-web-components-so-many-missing-features/23687


우리 팀 역시 디자인 시스템을 중심으로 한 UI 기준 정비 과정 속에서 기존 공통 컴포넌트들의 역할을 다시 정의할 필요가 있었습니다. 이는 특정 컴포넌트의 완성도 문제라기 보다, 팀 전체의 일관성과 유지보수를 고려한 선택의 결과에 가까웠습니다.





KID (Kakaopay insurance Design: 팀 자체 내에서 만든 디자인 시스템) 의 끝을 애도하는 FE 팀원들…

마치며

chacha: 그렇다면 가이드도 지키고 자유도도 가질 수 있는 방안은 없을까요?

pepe: 하나의 방안들 중 헤드리스 컴포넌트라는 것이 있더라구요.

헤드리스 컴포넌트란, unstyled 를 넘어 로직 기반 재사용성 / 확장성 / UI 커스터마이징 자유도를 제공하는 패턴 상태관리, 인터렉션로직, 접근성 처리와 같은 근본적인 동작만을 공통으로 제공하고 시각적인 표현은 사용처에서 결정하도록 위임하는 패턴입니다.

관련한 대표적인 라이브러리로 https://reach.tech/ 가 있습니다. 공식 문서를 보면 모든 컴포넌트들은 로직만을 제공하고 스타일은 전부 제외되어 있습니다.
웹에서 접근성을 고려한 컴포넌트를 직접 구현해 보면, Chrome, Safari 등 브라우저별 차이, 키보드 포커스 이동, ARIA 속성 처리 등 생각보다 많은 고려 사항과 공수가 필요하다는 것을 체감하게 됩니다.
모든 화면, 모든 컴포넌트에 대해 이런 접근성 로직을 매번 직접 구현하는 것은 현실적으로 쉽지 않습니다. 특히 빠른 개발이 요구되는 환경에서는 더더욱 그렇습니다. 이런 관점에서 공통 컴포넌트를 바라보니, 다음과 같은 구조도 충분히 가능하겠다는 생각이 들었습니다.

Headless 컴포넌트(로직 + 접근성만 제공하는 컴포넌트)를 베이스로 가이드에 맞게 스타일이 입혀진 기본 UI 버전과 서비스 특성에 맞게 커스텀이 가능한 자유 UI 버전으로 나누어 운영하는 방식이 있다면 어떨까.

디자인의 완전한 균일화만을 목표로 하지 않는 서비스라면… 모든 컴포넌트를 하나의 UI로 강제하기보다는 안정성과 접근성은 공통으로 가져가고, 디자인은 유연하게 허용하는 구조가 오히려 유지보수와 확장성 측면에서 하나의 돌파구가 될 수 있지 않을까라는 생각이 들었습니다



이 글에서 살펴본 공통 컴포넌트들의 모습은 어쩌면 사람의 생애주기와도 닮아 있습니다. 어떤 컴포넌트는 태어나며 빠르게 역할을 부여받고, 어떤 컴포넌트는 아직 준비되지 않은 채 독립을 요구받으며, 어떤 컴포넌트는 더 이상 선택되지 않으며 자연스럽게 역할을 내려놓습니다.


앞으로 우리 팀에서는 공통 컴포넌트를 건강하게 기르기 위해 의도적으로 더 많은 질문을 던지려 합니다.


공통 컴포넌트를 만들기 전에는


  • 이미 존재하는 컴포넌트를 조합해서 해결할 수는 없는지
  • 지금은 공통이 아니라고 말해도 괜찮은 시점은 아닌지
  • 나중에 공통으로 옮겨도 문제가 없는 구조인지

를 먼저 고민해 보려 합니다.

또 기존의 공통 컴포넌트를 사용할 때도 그저 쓰고 지나치기보다, 여전히 공통으로 유지하는 것이 합리적인지 PR과 리뷰 과정에서 질문을 남기려 합니다.
아이러니하게도, 지금 우리가 찾은 가장 현실적인 해결책은 공통 컴포넌트에 대해 더 자주, 더 솔직하게 질문하는 것이었습니다.
이 글은 그 질문들이 만들어진 과정에 대한 기록입니다.
그리고 이 기록이 여러분의 팀에서도 공통 컴포넌트를 대하는 질문을 조금 더 풍부하게 만드는 계기가 되기를 바랍니다.

pepe.do
pepe.do

카카오페이손해보험 클라이언트개발팀 프론트개발자 페페입니다. 더 나은 사용자 경험과 코드 사이에서 균형을 찾기 위해 노력하고 있습니다.

chacha.ah
chacha.ah

카카오페이손해보험 클라이언트개발팀 프론트엔드 개발자 차차입니다. 코드를 만들며 떠오른 질문을 기록으로 남깁니다.