디자인 엔지니어가 꼭 알아야 할 컴포넌트 API 설계와 제약의 조화

“이 버튼에만 특별히 외부 여백을 더 줄 수 없나요?”, “아이콘 색상만 살짝 바꾸고 싶은데 props가 안 열려 있네요.” 디자인 시스템을 운영하다 보면 매일같이 마주하는 요청들입니다. 모든 자유도를 허용하자니 시스템이 일관성을 잃고 파편화되고, 모든 것을 꽉 막아두자니 실무 개발자들이 시스템을 외면하고 직접 스타일을 덮어쓰기 시작합니다.
디자인 엔지니어의 핵심 역량은 단순히 예쁜 컴포넌트를 코딩하는 것이 아니라, 유연함과 제약 사이의 아슬아슬한 줄타기를 성공시키는 ‘컴포넌트 API’를 설계하는 데 있습니다. 이 글을 끝까지 읽고 나면, 유지보수의 늪에 빠지지 않으면서도 동료 개발자들에게 사랑받는 단단한 컴포넌트 인터페이스 설계법을 마스터하게 될 것입니다.
유연함의 역설: 왜 모든 것을 props로 만들면 안 될까
컴포넌트 설계 초기에 가장 많이 하는 실수가 ‘확장성’이라는 미명 아래 모든 스타일 속성을 props로 열어주는 것입니다. color, fontSize, padding 등을 각각의 props로 받게 되면 처음에는 편해 보이지만, 머지않아 시스템의 통제력을 잃게 됩니다. 디자인 시스템의 존재 이유는 ‘의사결정의 최소화’인데, 컴포넌트를 쓸 때마다 수많은 스타일 값을 고민해야 한다면 그것은 시스템이라기보다 단순한 유틸리티 모음에 가깝습니다.
훌륭한 API 설계는 사용자가 고민할 범위를 좁혀주는 것입니다. 예를 들어 color를 자유롭게 입력받는 대신, 미리 정의된 variant (primary, secondary, danger 등) 중 하나를 선택하게 유도해야 합니다. 이는 단순히 코드의 양을 줄이는 것이 아니라, 브랜드 가이드라인이라는 ‘제약’을 코드 수준에서 강제하는 강력한 장치가 됩니다.
[💡 에디터의 실무 팁: 컴포넌트의 props가 10개를 넘어가는 순간, 해당 컴포넌트가 너무 많은 책임을 지고 있는 것은 아닌지 의심해 보세요. 컴포넌트를 쪼개거나 ‘컴포지션(Composition)’ 패턴을 도입할 때라는 신호입니다.]
성공적인 API 설계를 위한 3가지 전략
1. 불리언(Boolean)보다는 열거형(String Union)을 선호하세요
처음에는 버튼에 isPrimary라는 props 하나만 있으면 충분해 보입니다. 하지만 시간이 지나 isSecondary, isGhost, isDanger가 추가되면 어떻게 될까요? 누군가 실수로 isPrimary와 isDanger를 동시에 true로 설정했을 때 어떤 스타일이 적용될지 예측하기 어려워집니다.
이를 variant="primary" | "danger" 형태의 문자열 유니온 타입으로 설계하면, 한 번에 하나의 상태만 가질 수 있도록 상호 배제적(Mutually Exclusive)인 로직을 강제할 수 있습니다. 이는 타입 안정성을 높일 뿐만 아니라 컴포넌트의 사용 의도를 명확하게 드러냅니다.
2. 스타일 확장(Style Overrides)의 퇴로를 열어두되, 명시적으로 처리하세요
아무리 완벽한 시스템이라도 특수한 상황(Edge Case)은 반드시 발생합니다. 이때 className이나 style을 무작정 막는 것보다는, containerProps나 boxProps처럼 명시적인 탈출구를 제공하는 것이 낫습니다. 혹은 Tailwind CSS를 사용한다면 twMerge와 같은 라이브러리를 활용해 시스템 기본 스타일과 외부 주입 스타일을 안전하게 병합하는 로직을 기본으로 탑재해야 합니다.
3. 슬롯(Slot) 패턴으로 컴포지션의 힘을 활용하세요
버튼 내부에 아이콘을 넣거나 특수한 배지를 넣어야 할 때, 매번 leftIcon, rightIcon 같은 props를 추가하는 것은 비효율적입니다. 리액트의 children이나 특정 영역을 비워두는 ‘슬롯’ 패턴을 활용해 보세요. 컴포넌트는 레이아웃과 핵심 로직만 담당하고, 내부의 세부 요소는 사용자가 조합해서 넣을 수 있게 하면 API가 훨씬 깔끔해집니다.
트러블슈팅: ‘Prop Explosion’ 현상 해결 사례
실제로 한 이커머스 프로젝트에서 버튼 컴포넌트의 props가 25개까지 늘어난 적이 있었습니다. 크기, 색상, 아이콘 유무, 로딩 상태, 모서리 곡률 등 모든 것이 props로 관리되다 보니 내부 로직이 수백 줄의 조건문으로 뒤덮여 유지보수가 불가능한 수준이었죠.
저는 이를 해결하기 위해 ‘기능 기반 분리’ 전략을 썼습니다. 모든 기능을 가진 만능 버튼(God Component)을 버리고, 기본 뼈대를 담당하는 BaseButton을 만든 뒤 이를 래핑한 ActionButton, IconButton, LinkButton으로 분리했습니다. 결과적으로 각 컴포넌트의 API는 5개 내외로 줄어들었고, 각자의 목적에 맞는 타입 힌트가 제공되어 개발 생산성이 2배 이상 향상되었습니다.
[💡 에디터의 실무 팁: API 설계가 막힐 때는 항상 “이 컴포넌트를 처음 쓰는 사람이 공식 문서를 보지 않고도 사용할 수 있는가?”를 자문해 보세요. 직관적인 네이밍이 기술적인 최적화보다 중요할 때가 많습니다.]
기술적 SEO와 접근성을 고려한 API
디자인 엔지니어의 API 설계는 시각적인 요소를 넘어 의미론적(Semantic)인 요소까지 책임져야 합니다. 예를 들어 버튼 컴포넌트가 내부적으로 <a> 태그로 렌더링될지 <button> 태그로 렌더링될지 결정하는 as 혹은 asChild props를 제공하는 것이 좋습니다. 이는 검색 엔진 최적화(SEO)와 스크린 리더를 사용하는 사용자의 접근성(Accessibility)에 직결되는 설계입니다.
또한, 클릭 이벤트가 발생하는 요소라면 aria-label이나 aria-expanded 같은 속성들을 props로 받아 적절한 위치에 주입해 주는 세심함이 필요합니다. 훌륭한 API는 개발자가 잊기 쉬운 접근성 가이드를 시스템 차원에서 자연스럽게 준수하도록 돕는 가이드라인이 되어야 합니다.
자주 묻는 질문(FAQ)
Q1. 디자인 토큰을 props로 직접 전달받는 게 좋나요? 아니요. padding={tokens.spacing.md}처럼 토큰 값을 직접 전달받기보다, spacing="md"처럼 추상화된 이름을 props로 받고 컴포넌트 내부에서 토큰과 매핑하는 것이 좋습니다. 그래야 나중에 토큰 체계가 바뀌어도 사용처의 코드를 수정하지 않을 수 있습니다.
Q2. 컴포넌트가 너무 유연성이 없다고 불평하는 동료들에겐 어떻게 대응하나요? 그 요청이 ‘반복적인 패턴’인지 ‘일회성 변칙’인지 먼저 파악하세요. 반복된다면 시스템에 정식 variant로 수용하고, 일회성이라면 시스템을 깨뜨리지 않는 선에서 extStyle 같은 제한적인 확장 prop을 제공하거나 컴포지션으로 해결하도록 가이드하세요.
Q3. 라이브러리(Headless UI 등)를 쓰는 게 API 설계에 도움이 되나요? 네, Radix UI나 Headless UI 같은 도구들은 이미 수많은 검증을 거친 API 구조를 가지고 있습니다. 이를 베이스로 삼아 우리 팀만의 스타일 토큰을 입히는 방식은 처음부터 모든 API를 설계하는 삽질을 줄여주는 아주 영리한 선택입니다.
