카카오페이 보험 FE팀이 공통 컴포넌트의 '사춘기'를 겪고 배운 것들

공통 컴포넌트를 건강하게 기르기

"이거 공통으로 빼야 할까?" 프론트엔드 개발자라면 누구나 한 번쯤 이 질문 앞에 서 봤을 거예요. 카카오페이 보험팀의 입사 4개월 차 차차와 3년 차 페페도 같은 질문에서 출발했습니다.

차차가 처음 레포지토리를 열었을 때 가장 먼저 든 생각. "공통 컴포넌트가 이렇게 많다니." 이전 회사에서는 경험하지 못한 규모였고, 쓸 수 있는 재료가 많다는 건 분명 장점처럼 느껴졌어요. 근데 동시에, 어디까지가 사용 가치가 있는 컴포넌트인지 알기 어려웠다고요.

차차: 공통 컴포넌트가 많으니 어떤 기준으로 써야 할지부터 막막해지더라고요. docs가 있었다면 구조를 이해하는 데 훨씬 수월했을 것 같아요.
페페: 지금 구조는 처음부터 이렇게 만들어진 건 아니에요. 상품이 늘어나면서, 재사용을 위해 조금씩 공통화가 진행됐고 그 과정에서 컴포넌트가 세밀해졌죠.

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

"공통으로 빼야 할까?" — 그 질문이 시작되는 순간

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

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

근데 구현부를 자세히 들여다보니 걸리는 지점들이 있었습니다.

StaticLabelTextArea 코드 분석

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

사용처 두 곳. deprecated 의존성. 향후 변경 시 감당해야 할 유지 비용이 더 커질 수 있다는 신호가 느껴졌다고 해요. 특히 의존하고 있는 하위 컴포넌트가 변경될 경우 그 영향 범위를 예측하기 어렵다는 점이 가장 큰 부담이었습니다. 이 조합이면 공통화는 오히려 부채가 될 수 있어요.

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

CoverageSelectCard의 사춘기 — 역할을 너무 많이 받아들인 컴포넌트

이렇게 만들어진 공통 컴포넌트들은, 시간이 지나면 예상치 못한 형태로 성장하기도 해요. 마치 사춘기를 겪는 사람처럼, 명확한 경계 없이 역할을 계속해서 받아들이다 보면 점점 본래의 목적과는 다른 모습이 되기도 합니다.

그럼 공통 컴포넌트가 '사춘기'를 겪으며 팀을 곤란하게 만들었던 몇 가지 패턴을 살펴볼게요.

공통에 있어 역할이 늘어나는 건 정말 올바른 방향일까? 여러 사람의 손이 들어가기 시작한 공통 코드는 시간이 지날수록 더 많은 요구사항을 받게 돼요. 초기에는 명확하고 단일한 목적을 가졌을지 몰라도, 재사용되기 시작하면서 새로운 역할이 하나둘 추가되는 거예요. 문제는 이 확장이 언제나 올바른 성장으로 이어지지 않는다는 점입니다. 기능과 가능성이 늘어날수록 책임 범위가 흐려지거든요.

CoverageSelectCard가 바로 그런 사춘기를 겪고 있어요.

CoverageSelectCard 구조

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

  • 바텀시트 열기/닫기 처리
  • iOS 환경 이슈 대응을 위한 body height 조작
  • 드래그 인터랙션 처리
  • 애니메이션 및 딜레이 제어
  • 각각의 로직은 그 시점에서 반드시 필요했던 기능이었고, 실제 문제를 해결하기 위한 선택이었어요.

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

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

    이건 보험 도메인만의 문제가 아니에요. 오래된 컴포넌트에서 흔히 관찰되는 성장 과정이죠.

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

    (솔직히 우리 프로젝트에도 하나쯤 있잖아요.) 특히 보험 도메인 특성상 복잡한 비즈니스 규칙과 다양한 예외 케이스가 존재한다는 점도 이런 비대화에 일정 부분 영향을 줬다고 합니다.

    중요한 건 CoverageSelectCard가 오랜 시간 동안 실제 문제를 해결해 왔다는 거예요. 그 과정에서 자연스럽게 많은 책임을 떠안게 된 거고요. 질문은 "이 컴포넌트가 나쁜가?"가 아니라, "이 역할들을 언제, 어떤 기준으로 나누는 것이 앞으로의 유지보수에 더 적합한가?"로 바뀌어야 합니다.

    너무 일찍 독립한 DelayRender

    모든 공통 컴포넌트가 비대해지는 방향으로 성장하는 건 아니에요. 어떤 컴포넌트들은 충분히 성장하기도 전에 "공통"이라는 이름을 먼저 갖게 됩니다.

    DelayRender 코드

    DelayRender(코드명 RenderAfter)는 일정 시간이 지나기 전까지는 fallback을 보여주고, 시간이 지나면 children을 렌더링하는 컴포넌트예요. 코드 크기도 작고, 특정 도메인에 얽혀 있지 않은 순수 UI 유틸리티 성격을 가지고 있습니다. 공통 컴포넌트로 분리한 선택이 당시로서는 합리적이었죠.

    근데 시간이 지나니 다른 관점의 질문이 생겼어요. "이 컴포넌트는 정말 공통으로 사용되고 있는가?" 현재 DelayRender는 소수의 화면에서만 사용되고 있었거든요. 이론적으로는 "딜레이 후 렌더링" 패턴이 다양한 화면에서 필요해질 수 있지만, 반대로 앱 내부에서 처리해도 충분한 규모이기도 했어요.

    이 지점에서 DelayRender는 공통 컴포넌트로 보아도 틀리지 않고, 그렇다고 이상적인 공통 컴포넌트라고 말하기도 어려운 경계선 위에 놓이게 돼요. 컴포넌트 자체는 작고 독립적이며 관리 부담도 크지 않아요. 각 사용처 내부에 두었더라도 큰 문제가 되지는 않았을 겁니다.

    공통 컴포넌트로서 충분히 합리적이었지만, 독립했다고 말하기에는 이른 상태에 가까웠어요. 이런 경우 중요한 건 정답을 정하는 것이 아니라 판단 기준을 팀 내에서 공유하는 일이에요.

  • 몇 번의 반복부터 공통으로 볼 것인가
  • 실제 사용 빈도와 관리 비용을 어떻게 저울질할 것인가
  • "지금은 공통이 아니다"라고 말할 수 있는 시점은 언제인가
  • DelayRender는 "공통 컴포넌트를 언제 만들 것인가"보다 "언제까지 공통으로 유지할 것인가"를 고민하게 만든 사례였어요. CoverageSelectCard가 너무 많은 역할을 떠안은 사춘기라면, DelayRender는 역할을 충분히 받기도 전에 독립해버린 케이스. 같은 "공통 컴포넌트" 문제인데 방향이 정반대라는 게 재밌어요.

    단일, 느슨한 조합, 템플릿 — 결합에도 단계가 있다

    차차: 그렇다면 템플릿은 어떤가요? 템플릿도 굉장히 많은데, 어떤 기준에서 만들어지게 된 건지 궁금해요!
    페페: 보험 도메인에서는 템플릿이 불가피하게 필요한 순간도 있다고 생각해요. 청약 씬을 보면 가입설계, 계약체결동의, 미리보기 문서, 계약자확인사항 등... 대부분의 상품 청약 플로우에서 반드시 거쳐야 하는 영역들이 있죠!

    컴포넌트 결합 단계

    컴포넌트의 결합 수준을 세 단계로 나눌 수 있어요.

    단일 컴포넌트 — 자유의 상태. 하나의 책임만 가져요. 입력을 받고, 정보를 보여주고, 상태를 표현하는 것처럼 명확하게 본인의 역할에만 집중합니다. 가장 유연하고, 가장 독립적인 상태.

    느슨한 조합 — 관계를 맺기 시작하는 상태. 여러 개의 단일 컴포넌트가 조합되어 하나의 화면이나 기능이 만들어지면, 이때 단일끼리 최소한의 관계가 생겨요. 아직 강한 결합은 아니지만, 함께 쓰이는 맥락이 생긴 거죠.

    템플릿 — 자유가 아닌 상태. 여러 컴포넌트를 단순히 묶은 형태가 아니라, 어떤 결정을 코드로 고정하는 구조예요. 페페의 말이 정확했어요. 보험 도메인에서는 청약 씬을 보면 가입설계, 계약체결동의, 미리보기 문서, 계약자확인사항 등 대부분의 상품 청약 플로우에서 반드시 거쳐야 하는 영역들이 있다고요. 이런 경우 템플릿이 불가피합니다.

    그래서 언제 결합하고, 언제 분리할까?

    결합을 선택해도 되는 그린 라이트가 있어요.

    첫째, 같은 조합이 복붙되고 있을 때.

    청약씬 반복 패턴

    청약씬에 존재하는 청약사항확인씬이 거의 동일한 경우가 이에 해당합니다. 복붙이 3회 이상 반복되면 그건 패턴이에요.

    둘째, 조합의 주체가 명확할 때.

    조합 주체가 명확한 경우

    누가 이 조합을 관리하고, 어떤 맥락에서 쓰이는지가 분명하다면 결합해도 괜찮습니다.

    반면 아직 템플릿이 아니라는 레드 라이트도 있어요.

    화면마다 예외가 생기는 경우

    화면마다 예외가 계속 생기거나, 불확실한 미래를 걸고 빼거나, 충분한 검증 기간과 케이스가 거쳐지지 않은 경우. 이럴 때는 아직 이르다는 신호예요.

    결국 템플릿은 재사용을 위한 도구가 아니라 "변경하지 않기로 합의한 결정 집합"으로 보는 게 더 정확해요. 이 구분이 생기면 판단이 훨씬 명확해집니다. "이걸 템플릿으로 빼야 하나요?"라는 질문에 "이 결정을 코드로 고정해도 되나요?"라고 바꿔서 물으면 답이 달라지거든요.

    공통 컴포넌트도 죽는다 — 그리고 그건 실패가 아니다

    원피스에 이런 명대사가 있죠.

    원피스 명대사

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

    비단 사람만의 이야기가 아니에요.

    차차: 공통 컴포넌트는 언제 죽었다고 판단할 수 있을까요?
  • 아무도 찾지 않을 때
  • 외부 정책 요인으로 deprecated될 때 (예: payfit 적용)
  • 로직이 너무 복잡해서 건들면 다 죽을 것 같을 때
  • 유기된 컴포넌트들

    애석하게도 어떤 컴포넌트들은 시간이 지나며 점점 아무도 손대지 않는 존재가 돼요. 시간이 지나며 옛날에 만들었던 공통 컴포넌트는 재사용의 이점을 제공하기보다 수정과 검증의 부담을 발생시키는 비용 요소가 됩니다. 이 시점에서 deprecated는 실패의 선언이 아니라 비용을 절감하기 위한 선택이에요.

    아마도 다수의 개발 조직들이 공통 컴포넌트를 영구 자산으로 보지 않을 거예요. 디자인 시스템의 진화, 조직 구조 변화, 유지 비용 증가 같은 이유로 의도적으로 deprecated를 선택하고 대체 컴포넌트를 제시하는 경우가 많습니다.

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

    Shopify Polaris deprecated 사례

    Shopify의 Polaris도 리액트 디자인 시스템을 deprecated한 뒤 고통받는 모습을 보여줬고요.

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

    KID 디자인 시스템의 종료를 애도하는 FE 팀원들

    KID(Kakaopay insurance Design), 팀 자체 내에서 만든 디자인 시스템의 끝을 애도하는 FE 팀원들의 모습이래요. (진지하게 애도 사진을 찍은 거 보면... 이 팀 분위기 좋아 보여요.)

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

    헤드리스 컴포넌트라는 돌파구, 그리고 더 중요한 질문

    차차: 그렇다면 가이드도 지키고 자유도도 가질 수 있는 방안은 없을까요?
    페페: 하나의 방안 중 헤드리스 컴포넌트라는 것이 있더라고요.

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

    대표적인 라이브러리로 reach.tech가 있어요. 모든 컴포넌트가 로직만 제공하고 스타일은 전부 제외되어 있습니다. Chrome, Safari 등 브라우저별 차이, 키보드 포커스 이동, ARIA 속성 처리 등 접근성을 직접 구현하면 생각보다 공수가 많이 들거든요. 빠른 개발이 요구되는 환경에서는 더더욱요.

    웹에서 접근성을 고려한 컴포넌트를 직접 구현해 보면, Chrome, Safari 등 브라우저별 차이, 키보드 포커스 이동, ARIA 속성 처리 등 생각보다 많은 고려 사항과 공수가 필요하다는 걸 체감하게 돼요. 모든 화면, 모든 컴포넌트에 대해 이런 접근성 로직을 매번 직접 구현하는 건 현실적으로 쉽지 않습니다. 빠른 개발이 요구되는 환경에서는 더더욱요.

    이런 관점에서 공통 컴포넌트를 바라보니, 이런 구조도 충분히 가능하겠다는 생각이 들었다고 해요. Headless 컴포넌트(로직 + 접근성만 제공)를 베이스로, 가이드에 맞게 스타일이 입혀진 기본 UI 버전과 서비스 특성에 맞게 커스텀 가능한 자유 UI 버전으로 나누어 운영하는 방식. 디자인의 완전한 균일화만을 목표로 하지 않는 서비스라면, 모든 컴포넌트를 하나의 UI로 강제하기보다는 안정성과 접근성은 공통으로 가져가고 디자인은 유연하게 허용하는 구조가 오히려 유지보수와 확장성 측면에서 돌파구가 될 수 있지 않을까요.

    이 글에서 살펴본 공통 컴포넌트들의 모습은 어쩌면 사람의 생애주기와도 닮아 있어요.

    StaticLabelTextArea는 태어나기 전에 멈춘 컴포넌트예요. "이거 공통으로 빼야 할까?" 단계에서 deprecated 의존성과 사용처 두 곳이라는 현실 앞에 태어나지 않기로 결정된 거죠. CoverageSelectCard는 오래 일한 컴포넌트예요. 바텀시트 열기/닫기, iOS 대응, 드래그 인터랙션, 애니메이션 제어까지 떠안으면서 사춘기를 겪고 있고요. DelayRender는 아직 준비되지 않은 채 독립을 요구받은 컴포넌트. KID 디자인 시스템은 더 이상 선택되지 않으며 자연스럽게 역할을 내려놓은 경우고요.

    카카오페이 보험 FE팀은 앞으로 공통 컴포넌트를 건강하게 기르기 위해 의도적으로 더 많은 질문을 던지려 한다고 해요.

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

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

    기존의 공통 컴포넌트를 사용할 때도 그저 쓰고 지나치기보다, 여전히 공통으로 유지하는 것이 합리적인지 PR과 리뷰 과정에서 질문을 남기기로 했습니다. 단순히 "사용 가능하니까 쓴다"가 아니라, "이게 아직 공통으로 남아 있어야 하는가"를 주기적으로 점검하겠다는 거예요.

    아이러니하게도, 지금 이 팀이 찾은 가장 현실적인 해결책은 정답을 정하는 게 아니었어요. 공통 컴포넌트에 대해 더 자주, 더 솔직하게 질문하는 것. 정답이 아니라 질문을 늘린 거예요.

    이 글은 그 질문들이 만들어진 과정에 대한 기록이에요. 그리고 이 기록이 여러분의 팀에서도 공통 컴포넌트를 대하는 질문을 조금 더 풍부하게 만드는 계기가 되면 좋겠습니다.