“카드에 border-radius를 4px로 줄까요, 8px로 줄까요?”
47분짜리 스레드였습니다. 엔지니어 넷, 디자이너 둘, 변형이 여섯 개 들어간 Figma 파일 하나. 논의는 iOS 컨벤션에서 Material Design 가이드라인, 시각적 정렬, 급기야 황금비율까지 뻗어 나갔습니다. 아무도 틀린 말을 하지 않았습니다. 바로 그게 문제였습니다.
이런 상황은 처음이 아니었습니다. border-radius만의 문제가 아니라, 그라데이션 방향, 섀도우 블러 값, 비활성 상태의 opacity 수치까지 전부 마찬가지였습니다. 하나하나의 결정은 사소했습니다. 하나하나의 논쟁은 합리적이었습니다. 그리고 이것들이 합쳐지면서 팀의 속도를 갉아먹고 있었습니다.
그래서 올바른 답을 찾는 것을 그만뒀습니다. 질문 자체를 없앴습니다.
선택지의 비용
디자인 시스템의 모든 시각적 속성은 의사결정 포인트입니다. border-radius 하나만 놓고 봐도 질문이 쏟아집니다. 버튼과 카드에 같은 값을 써야 할까? 중첩된 요소는 안쪽 radius에서 바깥쪽 값을 빼야 할까? 크기에 따라 radius를 달리해야 할까?
이 질문들 중 어려운 건 하나도 없습니다. 바로 그 점이 위험합니다. 누구나 의견을 낼 수 있을 만큼 쉽지만, 어떤 의견이 명백히 더 낫다고 증명할 수 없을 만큼 사소합니다. 전형적인 바이크셰딩(bikeshedding) — 핵심이 아닌 곳에 에너지가 쏠리는 현상입니다.
한 분기 동안 PR 코멘트를 직접 분류해 봤습니다. 디자인 시스템 리뷰 스레드의 약 30%가 사용성에 아무런 영향이 없는 장식적 값에 대한 것이었습니다. 레이아웃도 아니고, 접근성도 아니고, 인터랙션 패턴도 아닌, 순수하게 미적인 논의였습니다.
그때 규칙을 만들었습니다.
제한이 아니라 제거인 이유
가장 자연스러운 대안은 속성 자체는 유지하되 선택지를 줄이는 것입니다. 대부분의 성숙한 디자인 시스템이 그렇게 합니다 — border-radius 허용 값 세 개, 섀도우 엘리베이션 스케일, 그라데이션 팔레트. 저희도 검토했습니다.
문제는 제한된 선택지도 여전히 논쟁을 만든다는 점입니다. “이 카드에 radius-sm을 쓸까요, radius-md를 쓸까요?”는 답의 개수만 줄었을 뿐 본질적으로 같은 대화입니다. 여럿보다 적은 게 낫지만, 적은 것보다 없는 게 낫습니다. 속성을 제한하면 결정이 줄어들고, 속성을 제거하면 결정 자체가 사라집니다.
린팅도 살펴봤습니다. 허용되지 않은 값을 거부하는 ESLint 규칙을 만드는 방식입니다. 하지만 “0, 4, 8px만 허용”하는 린터에도 여전히 세 가지 선택지가 남습니다. 목표는 결정을 쉽게 만드는 것이 아니라, 결정을 불필요하게 만드는 것이었습니다.
네 가지 규칙
Beaket의 컴포넌트 라이브러리에는 네 가지 시각적 제약이 있습니다.
- border-radius 금지. 모든 요소에 직각 모서리를 적용합니다. 버튼, 카드, 인풋, 다이얼로그, 툴팁 — 전부 직사각형입니다.
- 그라데이션 금지. 단색만 사용합니다. 모든 표면은 하나의 CSS 변수입니다.
- 블러 섀도우 금지. blur radius가 있는
box-shadow는 사용하지 않습니다. 오프셋 섀도우만 허용됩니다. - 장식적 opacity 금지. 비활성 상태는 색상 변경으로 처리합니다.
opacity: 0.5같은 편법은 쓰지 않습니다.
이것들은 가이드라인이 아닙니다. 코드 리뷰에서 강제하고, 컴포넌트 가이드라인에 문서화한 엄격한 규칙입니다. rounded-md를 추가한 PR은 리젝됩니다.
왜 하필 이 네 가지 속성일까요? 사용성 영향은 가장 적으면서 논쟁은 가장 많이 발생시킨 속성들이기 때문입니다. 여백과 타이포그래피는 가독성에 영향을 줍니다. 색상은 의미를 전달합니다. 하지만 border-radius가 4px인지 8px인지, 블러 섀도우가 4px인지 6px인지에 따라 사용자의 제품 이용에 측정 가능한 차이가 생긴 사례는 단 하나도 찾지 못했습니다.
규칙을 도입한 뒤, 디자인 시스템 PR의 진행 속도가 빨라졌습니다. 리뷰 스레드가 짧아졌습니다. 디자이너들은 모서리 처리나 섀도우 깊이만 다른 동일 컴포넌트 변형을 더 이상 만들지 않았습니다. 논쟁이 줄어든 것이 아니라, 아예 사라졌습니다.
대신 사용하는 것들
선택지를 없앤다고 시각적 위계까지 없어지는 것은 아닙니다. 깊이감, 상태, 인터랙션은 여전히 전달해야 합니다. 각 속성을 어떻게 대체했는지 설명하겠습니다.
블러 대신 오프셋 섀도우
box-shadow: 0 4px 6px rgba(0,0,0,0.1) 대신, CSS 변수로 정의된 하드 오프셋 섀도우를 사용합니다.
--shadow-offset: 1px 1px 0px 0px var(--color-chrome);
--shadow-offset-hover: 2px 2px 0px 0px var(--color-chrome);
--shadow-offset-active: 0px 0px 0px 0px var(--color-chrome);
인터랙션 상태 세 개. 블러 없음. 이것이 섀도우 시스템의 전부입니다.
기본 상태는 1px 오프셋으로, 요소가 표면에서 살짝 떠 있는 느낌을 줍니다. 호버 시 2px로 늘어나며 미세하게 들어올려집니다. 액티브 시 0px로 돌아가며 요소가 바닥에 눌리는 느낌을 줍니다. 작고 기계적인 애니메이션이지만, 인터랙션을 명확히 전달하며 개발자가 설정할 것이 아무것도 없습니다.
실제로 모든 인터랙티브 컴포넌트에 동일한 세 개의 토큰이 적용됩니다. 아래는 버튼 컴포넌트의 기본 구조입니다(비활성 및 포커스 상태를 포함하는 전체 CVA 문자열에서 간략화한 버전입니다).
const buttonVariants = cva(
[
"inline-flex items-center justify-center gap-2",
"font-medium cursor-pointer",
"shadow-offset hover:shadow-offset-hover active:shadow-offset-active",
"transition-shadow duration-100",
// ... disabled, focus-visible, and icon styles
].join(" "),
// ... variants
);
어떤 컴포넌트에 어떤 섀도우 레벨이 “적합한지” 고민할 필요가 없습니다.
CSS 변수를 통한 단색 시스템
저희의 색상 팔레트는 “Porcelain”이라 부르며, graphite(#030509)부터 paper(#ffffff)까지 14단계 중성색으로 구성되어 있습니다. 차갑고 푸른 톤을 띱니다. 문서에 적힌 테마 설명은 이렇습니다. “산업적 정밀함. 검은 합성 래커로 컨트롤 패널에 새겨진 듯한. 순백, 차가운 청흑 잉크, 유령 같은 그림자, 틸 액센트.”
모든 색상 참조는 시맨틱 토큰을 거칩니다.
--color-text-primary: var(--color-graphite);
--color-text-secondary: var(--color-steel);
--color-bg-primary: var(--color-paper);
--color-border-default: var(--color-chrome);
bg-white나 text-gray-500 같은 Tailwind 기본 색상은 사용하지 않습니다. 모든 색상은 이름이 붙은 변수이고, 모든 변수는 역할에 매핑됩니다. gray-100을 쓸지 gray-200을 쓸지 논쟁할 일이 없습니다 — 시맨틱 토큰이 이미 답을 내려놓았기 때문입니다.
어디서나 직각 모서리
이 항목은 대체할 것이 없습니다. 그냥 border-radius를 넣지 않으면 됩니다.
놀라웠던 점은 직각 모서리와 오프셋 섀도우가 만났을 때의 조화입니다. 둥근 카드에 오프셋 섀도우를 넣으면 어딘가 어색합니다 — 섀도우의 날카로운 가장자리와 부드러운 모서리가 충돌합니다. 하지만 직각 카드에 오프셋 섀도우를 넣으면 의도된 디자인으로 읽힙니다. 지오메트리가 일관적입니다. 제약들이 서로를 강화하는 방식은 저희도 처음에 예상하지 못한 것이었습니다.
빼기만으로 시각적 언어가 일관성을 갖게 됩니다. 모든 것이 직각이면, 아무것도 자신의 형태를 정당화할 필요가 없습니다.
제약이 복리로 작용하는 지점
이 접근법의 가장 날카로운 이점은 모든 컴포넌트에 걸쳐 적용되는 크로스커팅 패턴에서 나타납니다.
비활성 상태는 컴포넌트 라이브러리 전반에서 일관성이 무너지기로 악명 높습니다. 어떤 것은 opacity를 쓰고, 어떤 것은 배경색을 바꾸고, 어떤 것은 둘 다 합니다. 저희는 모든 컴포넌트에 하나의 패턴만 적용합니다.
disabled:shadow-none disabled:cursor-not-allowed disabled:border-dashed disabled:border-chrome disabled:bg-frost disabled:text-steel
버튼, 인풋, 셀렉트, 체크박스, 스위치 — 전부 동일합니다. 섀도우가 사라지고(인터랙션 어포던스가 제거됨), 점선 보더, frost 배경, steel 텍스트. 비활성 컴포넌트를 하나 보면 전부 본 것과 같습니다. 제약이 비활성 상태를 각 컴포넌트의 “시각적 개성”에 맞추려는 유혹을 제거했습니다. 컴포넌트에 시각적 개성이 없으면, 비활성 상태는 자연스럽게 수렴합니다.
포커스 링도 마찬가지입니다. 포커스 가능한 모든 컴포넌트에 동일한 스타일이 적용됩니다.
focus-visible:outline-2 focus-visible:outline-signal-blue focus-visible:outline-offset-2
색상 하나. 두께 하나. 오프셋 하나. 키보드로 폼을 탐색하는 사용자는 모든 요소에서 정확히 같은 포커스 링을 봅니다. 어떤 컴포넌트도 이를 오버라이드하지 않고, 어떤 변형도 이를 제거하지 않습니다. 통일된 비활성 패턴과 결합하면, 사용자는 어떤 컴포넌트와 상호작용하든 “비활성”과 “포커스”가 항상 같은 모습이라는 것을 알 수 있습니다.
유일한 예외
라디오 버튼에는 rounded-full을 사용합니다.
끝입니다. 전체 컴포넌트 라이브러리에서 유일한 예외입니다. 이 예외는 어떤 규칙을 위반하는지와 함께 컴포넌트 가이드라인에 명시되어 있습니다. 예외조차 우연이 아니라 의도된 결정이어야 하기 때문입니다.
정사각형 라디오 버튼은 사용자를 혼란에 빠뜨립니다. 원형이라는 형태가 인터페이스 관습에 너무 깊이 자리 잡은 나머지, 이를 제거하면 미적 순수성을 위해 사용성을 희생하는 셈이 됩니다. 그건 나쁜 거래라고 판단했습니다.
이런 의문이 들 수 있습니다. 사용성이 라디오 버튼의 예외를 정당화한다면, 아바타, 태그, 토글 스위치는 왜 안 되는가? 테스트했습니다. 직각 아바타에 오프셋 섀도우를 넣으면 프로필 카드처럼 보입니다 — 의도된 디자인으로 읽힙니다. 태그와 토글은 라디오 버튼만큼 관습에 묶여 있지 않습니다. 원형 어포던스가 유독 강한 것은 라디오 버튼뿐이며, 다른 요소는 그 기준을 넘지 못했습니다.
목표는 “브루탈리즘 그 자체”가 아니었습니다. 자신의 복잡성을 정당화하지 못하는 시각적 결정을 제거하는 것이었습니다. 둥근 라디오 버튼은 자신의 형태를 정당화합니다. 둥근 카드는 그렇지 않습니다.
실제 비용
이 접근법에 단점이 없다고 말하면 거짓말일 것입니다. 분명히 있습니다.
“미완성처럼 보여요.” 사용자 테스트에서 실제로 나온 반응입니다. 직각 모서리와 단색은 둥글고 그림자가 있는 인터페이스가 주는 완성도를 전달하지 못합니다. 이유를 설명해야 했던 적이 한두 번이 아닙니다.
일부 컴포넌트가 딱딱하게 느껴집니다. 특히 툴팁과 팝오버는 직각 모서리일 때 무겁게 보입니다. 블러 섀도우가 존재하는 이유가 있습니다 — 오프셋 섀도우로는 완전히 재현하기 어려운 부유감을 만들어 냅니다.
서드파티 컴포넌트가 패턴을 깨뜨립니다. 데이트피커, 리치 텍스트 에디터, 차트 라이브러리 — 전부 border-radius와 블러 섀도우가 내장되어 출시됩니다. 오버라이드할 수 있는 것은 오버라이드하고, 일부 불일치는 감수합니다. 이것은 해결된 문제가 아니라 현재 진행형 과제입니다.
디자이너의 표현이 제한됩니다. 그라데이션 오버레이와 미묘한 깊이감으로 마이크로 인터랙션을 다듬는 데서 즐거움을 느끼는 디자이너라면, 이 시스템이 답답할 것입니다. 그게 목적이긴 합니다만, 창작 만족도에 대한 실질적인 비용인 것은 분명합니다.
호불호가 갈립니다. 단순함을 좋아하는 엔지니어도 있고, 독단적이라고 생각하는 엔지니어도 있습니다. 이 긴장을 완전히 해소하지는 못했습니다. 다만 우리 제품에서는 일관성의 이점이 미적 비용을 상회한다고 판단했을 뿐입니다.
아직 모르는 것들
이 시스템을 진정한 규모에서 검증하지는 못했습니다 — 수백 개의 컴포넌트, 데이터 밀도가 높은 인터페이스, 네이티브하게 보여야 하는 서드파티 통합. 테이블, 차트, 대시보드는 오프셋 섀도우만으로는 부족한 깊이감 단서가 필요할 수 있습니다.
저희의 섀도우 상태(기본 → 호버 → 액티브)에 대해 사용자가 기존 엘리베이션 시스템과 같은 수준의 직관적 이해를 형성하는지도 솔직히 확신하지 못합니다. 사용성 저하가 관찰된 적은 없지만, 이 부분에 대한 정식 A/B 테스트를 실시하지는 않았습니다. 현재까지는 잘 작동하고 있습니다. 하지만 “현재까지”는 작은 표본입니다.
제약은 곧 결정이다
이 시스템의 모든 제약은 한 번 내린 결정이며, 다시는 내릴 필요가 없는 결정입니다. border-radius 금지는 미적 취향이 아닙니다 — 전체 컴포넌트 라이브러리에 대한 ADR(Architecture Decision Record)입니다.
팀이 사용성에 영향을 주지 않는 시각적 속성을 두고 시간을 쓰고 있다면, 선택지를 아예 없애 보는 것을 고려해 보십시오. 생각보다 그리워하지 않을 수 있습니다.