pile·
프론트엔드·카카오 엔터테인먼트 FE카카오 엔터테인먼트 FE·

더 가치 있는 공통 컴포넌트 만들기

React 공통 컴포넌트를 단순 재사용 단위를 넘어 쓸모 있게 만들려면 무엇을 고민해야 하는지 세 축으로 짚는 글이다. 확장 규칙 설계, 네이티브 요소 활용, 웹 접근성 순으로 가며 각 축마다 TypeScript와 React 코드를 붙였다. 공통 컴포넌트의 정의나 철학처럼 팀마다 다른 부분은 의도적으로 빼고 구현 판단에 집중한다.

핵심 포인트
  • 확장성을 논하기 전에 컴포넌트의 역할 경계부터 고정해야 한다. 만능 컴포넌트를 지향하면 결국 빈 DOM 요소가 된다.
  • 스타일 분기는 variant prop으로 흡수하고 나머지는 DOM attribute 인터페이스 상속과 rest parameters로 넘긴다.
  • 다만 attribute 인터페이스를 무조건 상속하는 것은 사용자의 prop 파악과 리팩터링 검증을 어렵게 만든다.
  • 체크박스 같은 폼 컴포넌트는 직접 그리지 말고 네이티브 요소를 투명하게 덮어 쓰는 편이 완성도가 높다.
  • 폼 컴포넌트는 forwardRef로 감싸야 react-hook-form처럼 ref를 쓰는 라이브러리와 붙는다.
  • role과 ARIA 속성을 붙이면 스크린 리더뿐 아니라 테스트 코드에서 요소를 의미론적으로 선택할 수 있다.
상세 정리
  • 문제 인식: 재사용하려고 만든 공통 컴포넌트인데 패딩이나 텍스트 크기가 조금 다르거나 onTouchStart 같은 드문 메서드가 필요해 그대로 못 쓰는 경우가 많다. prop으로 다 받으면 공통 컴포넌트의 가치가 떨어진다.
  • 역할 경계: 프로젝트에서 반복되는 버튼의 기본형이라는 책임을 줬다면 전혀 다른 디자인의 버튼까지 감당할 필요가 없다.
  • 분리 예시: className만 합성하는 BaseButton을 두고 그 위에 서비스별 버튼 컴포넌트를 얹는 편이 생산성과 유지보수에 낫다고 본다.
  • 재사용성과의 맞바꿈: 역할을 좁히면 당장은 재활용성이 떨어져 보이지만 확장을 고민하기 시작할 때 생산성을 지키는 장치가 된다.
  • variant 도입: TailwindCSS와 classnames로 className을 매번 조합하는 것은 번거롭고 휴먼 에러를 부르므로 primary나 secondary 같은 variant로 자주 쓰는 조합을 미리 묶는다.
  • rest parameters: ButtonHTMLAttributes를 상속하면 남은 attribute를 전개로 넘길 수 있다. 단 rest를 추가했다고 기존 prop을 느슨하게 푸는 것은 잘못된 방향이다.
  • 상속의 단점 1: 사용자가 각 prop의 역할을 파악하기 어렵고 오버라이딩된 prop인지 알 수 없어 예상 밖으로 동작할 수 있으며, IDE 자동완성에 불필요한 prop이 함께 뜬다.
  • 상속의 단점 2: 리팩터링 시 겉보기 동작이 유지돼야 하는데 잘 안 쓰이는 prop까지 사용되고 있으면 모든 케이스를 검증하기 어렵다. 미래 가능성을 모두 열어두는 선제적 설계를 권장하지 않는다는 마틴 파울러의 입장을 근거로 든다.
  • 네이티브 숨기기: div와 아이콘으로 체크박스를 만들면 disabled 처리나 탭 포커스 같은 기본 동작을 직접 다 구현해야 한다.
  • 구현 방법: 아이콘 위에 input type checkbox를 position absolute와 opacity 0으로 겹쳐 두면 사용자가 아이콘을 클릭해도 실제로는 네이티브 체크박스가 눌린다. label을 쓰거나 hidden 타입을 쓰는 변형도 가능하다.
  • controlled 선택 기준: 값을 상태로 관리하므로 다른 컴포넌트가 계속 값을 바라보며 리렌더링해야 한다면 적합하다.
  • controlled 비용: 값이 바뀔 때마다 부모까지 state lifting이 필요하고 하위 전체가 리렌더링되며, CheckboxGroup 같은 상위 컴포넌트를 만들면 prop drilling이 생긴다.
  • 절충안: 내부적으로는 controlled로 돌면서 사용자에게는 uncontrolled처럼 보이는 인터페이스를 필자가 선호한다. checked prop이 undefined인지로 제어 여부를 판별한다.
  • forwardRef: ref는 일반 prop으로 전달되지 않으므로 모든 인풋 계열 공통 컴포넌트는 forwardRef로 감싸 export하기를 권한다.
  • 시맨틱 태그: 단락 텍스트는 div 대신 p를 쓰는 식으로 역할을 드러내면 SEO와 스크린 리더 외에 기본 속성 적용 이점도 얻는다.
  • ARIA 예시: 탭은 role을 tab, tablist, tabpanel로 나누고 aria-selected, aria-controls, aria-labelledby, aria-hidden을 채운다. id는 재사용 시 중복되지 않도록 주의한다.
  • 테스트 이점: aria-labelledby 덕분에 getByLabelText로 패널을 선택해 선택된 탭만 보이는지 검증할 수 있다.
  • 편의성 인터랙션: ESC로 모달 닫기, 방향키로 드롭다운 포커스 이동처럼 네이티브가 주지 않는 동작은 직접 구현해야 UX가 완성된다.
왜 읽나사내 공통 컴포넌트가 prop만 계속 늘어나고 있다면 역할 경계, 인터페이스 상속 범위, 네이티브 위임이라는 세 기준으로 다시 그을 수 있다.
카카오 엔터테인먼트 FE
카카오 엔터테인먼트 FE 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 프론트엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    항공 프론트엔드 구축기 (7/10): 창구를 하나만 두었습니다

    여기어때 항공 서비스 프론트엔드가 웹과 앱 웹뷰 두 환경에서 동일한 함수 호출로 동작하는 앱 브릿지 추상화 레이어를 설계한 과정을 다룬다. iOS·안드로이드 규약 차이와 "웹에 존재하지 않는 브릿지를 어떻게 호출하나"라는 문제를 단일 추상화 층으로 해결한 구현 사례다.

    요약 이어보기
    #react#typescript#webview+2