AI가 읽는 디자인 시스템: 추상화를 다시 설계한 이유
규칙을 더 잘 설명하는 대신, 규칙을 어길 수 없게 만들기
Timo • Core Web
- Frontend
안녕하세요, 코어 웹팀에서 프론트엔드 공통 기반과 디자인 시스템을 만들고 있는 티모입니다.
제품 기능을 만드는 피처팀이 빠르게 이터레이션을 돌며 가설을 검증하는 동안, 코어팀은 여러 팀이 함께 사용하는 기반이 무너지지 않도록 중심을 잡는 일을 합니다. 빠르게 만드는 것만큼, 빠르게 만들어도 망가지지 않는 경계를 설계하고 고민합니다.
이제 이 경계를 지키며 코드를 작성하는 주체에는 AI도 포함됩니다. AI가 제품 코드를 직접 작성하기 시작하면서, 만드는 속도뿐 아니라 공통 기반의 의도를 벗어나지 않도록 돕는 장치가 중요해졌습니다. 그래서 AI가 코드를 더 잘 작성하도록 시스템 프롬프트를 다듬고, 스킬을 만들고, 필요한 문서를 찾아주거나 과 잘못된 코드를 잡는 하네스를 붙입니다. 채널톡에서도 이런 도구를 적극적으로 만들고 있습니다.
여기서 하네스는 AI 주변에 맥락과 규칙, 검증 루프를 붙여 결과가 의도한 방향으로 가도록 돕는 장치 전반을 뜻합니다.
그런데 채널톡의 디자인 시스템인 Bezier(베지어)를 크게 개편하면서 조금 다른 질문을 하게 됐습니다.
AI에게 규칙을 더 잘 설명하는 것과, 규칙을 어길 수 없게 만드는 것 중 무엇이 더 강한 가이드일까?
이 질문에 대한 답을 Button과 item류 컴포넌트의 실제 설계 과정을 통해 풀어보겠습니다.
사람뿐 아니라 AI도 읽기 좋은 디자인 시스템
최근 채널톡은 큰 리디자인을 거쳤습니다. 자연스럽게 디자인 시스템도 새 UI에 맞춰 개편해야 했습니다. 그러나 이번 개편의 목표는 UI의 모양을 새로 맞추는 데 그치지 않았습니다. 사람뿐 아니라 AI도 컴포넌트의 이름과 인터페이스만 보고 목적과 사용법을 이해할 수 있는 디자인 시스템을 만들고 싶었습니다.
기존의 디자인 시스템은 개발자가 필요한 UI를 빠르게 조립하도록 도와줬습니다. 하지만 자유도가 큰 인터페이스는 그만큼 많은 판단을 사용하는 쪽에 넘겼습니다. 어떤 HTML 요소로 렌더링할지, 어떤 접근성 역할을 가져야 하는지, 클릭과 키보드 입력을 어떻게 같은 동작으로 연결할지 등을 매번 제품 코드에서 다시 정해야 했습니다.
사람은 문서와 관습, 주변 코드를 함께 보며 빈칸을 채울 수 있습니다. AI도 비슷하게 추론할 수 있지만, 추론해야 할 빈칸이 많아질수록 결과는 매번 달라질 수 있습니다. 같은 지시문도 문맥과 실행마다 다르게 해석할 수 있기 때문입니다.
반면 타입과 인터페이스는 허용되는 상태를 결정론적으로 제한합니다.
베지어의 Button을 예로 들어보겠습니다. 매번 AI에게 "버튼 라벨에는 아이콘이나 레이아웃 요소를 넣지 마세요" 라고 알려줄 수도 있습니다. 하지만 Button이 자유로운 children 대신 필수 label: string을 받으면 규칙은 설명이 아니라 계약이 됩니다.
라벨과 아이콘을 함께 표현할 수는 있지만 역할이 다른 값을 한 children안에 섞지는 않습니다. 텍스트는 label, 장식이나 보조 콘텐츠는 leadingContent와 trailingContent로 자리를 나눴습니다. 아이콘만 있는 액션은 별도의 IconButton이 맡고 accessible name을 위한 aria-label을 필수로 요구합니다.
필요한 확장을 전부 막는 것이 아니라, 변화의 축마다 이름과 책임을 부여한 것입니다. custom wrapper가 필요한 경우에는 as라는 탈출구를 열어두되, Button의 라벨 계약은 그대로 유지합니다. 중요한 것은 기본 경로가 컴포넌트의 목적을 가장 잘 표현하고, 예외를 선택했을 때 누가 책임을 갖는지가 분명해야 한다는 점입니다.
좋은 추상화는 가장 많은 것을 금지하는 인터페이스가 아닙니다. 컴포넌트가 반드시 지켜야 하는 불변조건과 사용하는 쪽이 선택해야 하는 변화를 구분하는 인터페이스입니다.
규칙을 문서로 설명하는 것과 잘못된 상태를 표현할 수 없게 만드는 것은 다릅니다. 이 차이가 가장 크게 드러난 사례가 item류 컴포넌트였습니다.
비슷하게 생긴 행은 같은 컴포넌트일까?
기존 베지어에는 ListItem이라는 아토믹 컴포넌트만 있었습니다. 이 컴포넌트는 아이콘과 텍스트가 좌우로 놓인 행의 공통된 모양을 제공했지만, 사용처에 맞는 세부 레이아웃과 접근성, 상호작용은 제품 코드에서 매번 직접 작성해야 했습니다. 다만 드롭다운 메뉴의 항목, 설정 화면의 콘텐츠 행, 사이드바의 내비게이션이 시각적으로 비슷했기 때문에, 하나의 ListItem을 기반으로 여러 UI를 빠르게 만들 수 있었습니다.
처음부터 이를 여러 컴포넌트로 나누기로 결정한 것은 아닙니다. 기존 사용처가 많고 역할이 섞여 있었기 때문에, 리디자인에 따른 스타일만 반영하고 기존 ListItem을 그대로 유지하는 방안도 검토했습니다. 이 경우에는 기존과 마찬가지로 각 사용처가 맥락과 접근성, 상호작용을 계속 책임집니다. 마이그레이션 비용과 일정만 보면 충분히 합리적인 선택이었습니다.
문제는 이 세 가지가 시각적으로만 비슷할 뿐, 같은 역할은 아니라는 것입니다.
드롭다운의 항목은 액션을 실행합니다. 위쪽과 아래쪽 방향키로 이동하고, Enter나 Space로 선택하며, 선택 후 메뉴를 닫아야 합니다.
일반 목록의 행은 정보를 보여줄 수도 있고, 버튼이나 링크가 될 수도 있습니다. 메뉴처럼 행 사이의 키보드 탐색을 소유하면 오히려 안 됩니다.
내비게이션의 항목은 URL로 이동하고 현재 페이지를 표현합니다. 중첩된 항목이 있다면 하위 목록을 여닫는 disclosure의 책임도 필요합니다.
범용 ListItem은 이런 목적을 알지 못했습니다. interactive, active, href, as 같은 옵션으로 모양과 HTML 요소를 바꿀 수는 있었지만, "이 행은 메뉴 액션이다" 또는 "이 행은 현재 위치를 나타내는 내비게이션이다"라는 의미를 소유하지 않았습니다. 결국 사용하는 쪽에서 role, 포커스 이동, 선택 상태, 키보드 이벤트를 다시 조립해야 했습니다.
AI에게도 어려운 인터페이스입니다. 화면에서 아이콘과 텍스트가 있는 행을 발견하면 ListItem까지는 쉽게 고를 수 있습니다. 하지만 그다음부터는 긴 지시문이 필요합니다.
메뉴 안에서는 적절한 role을 추가하고, 방향키와 Enter를 지원하고, 비활성 항목은 건너뛰고, 선택 후에는 오버레이를 닫아라.
AI가 이 규칙을 잘 따르도록 하네스를 만들 수는 있습니다. 하지만 같은 규칙을 모든 사용처가 반복해서 알아야 한다면, 책임이 잘못된 곳에 있는 것은 아닐까요?
모양이 아니라 목적을 선택하게 만들기
개편에서는 ListItem을 그대로 공개하지 않고, 사용 목적에 따라 DropdownMenu, Section, NavigationList로 API를 나눴습니다. 각 컴포넌트가 자기 맥락의 행동과 의미를 소유합니다. 다만 아이콘과 텍스트가 놓이는 행의 마크업과 스타일은 반복되므로, 이 부분만 외부에 노출하지 않는 BaseItem으로 공통화했습니다.
BaseItem은 제품 개발자가 직접 선택하는 컴포넌트가 아닙니다. DropdownMenuItem, SectionItem, NavigationItem이 내부에서 BaseItem을 렌더링해 크기, 색상, leading content와 trailing content의 배치를 공유합니다. menu role, option selection, 키보드 동작은 BaseItem에 넣지 않고 각 공개 컴포넌트가 맡습니다.
DropdownMenu: 메뉴 액션 소유
DropdownMenuItem은 단순히 클릭 가능한 행이 아닙니다. 메뉴 안에서 포커스를 이동하고, Enter와 Space를 포인터 선택과 같은 동작으로 연결하고, 비활성 항목을 건너뛰고, 선택 뒤 메뉴를 닫는 흐름을 소유합니다.
그래서 이벤트 이름도 일반적인 onClick 대신 onSelect를 사용합니다. "마우스로 클릭했을 때"가 아니라 "어떤 입력 수단으로든 이 메뉴 항목이 선택됐을 때"가 컴포넌트의 본질이기 때문입니다.
타입도 메뉴 액션과 링크를 동시에 표현하지 못하도록 나눴습니다. 아래는 실제 구조를 단순화한 예시입니다.
href가 있는 항목은 native anchor가 되고, 메뉴 액션은 onSelect 경로를 탑니다. 링크에 메뉴 액션의 닫힘 정책을 섞는 식의 모호한 조합은 타입 단계에서 막습니다.
Trigger에도 aria-haspopup, aria-controls와 target ref가 자동으로 연결됩니다. 사용하는 쪽은 접근성 속성을 하나씩 외우기보다 메뉴라는 목적을 선택하면 됩니다.
Section: 콘텐츠의 묶음과 행 소유
SectionItem은 메뉴가 아닌 일반 콘텐츠 행입니다. 아무 이벤트도 없으면 비상호작용 div, onClick이 있으면 네이티브 button, href가 있으면 네이티브 a로 렌더링됩니다. 개발자가 as 값을 따로 맞추지 않아도 데이터와 행동에 따라 올바른 기본 요소를 선택합니다.
이 동작은 as prop을 사용하는 쪽이 직접 맞추는 대신, href와 onClick의 유무를 기준으로 한 union type으로 표현했습니다.
따라서 클릭 이벤트가 있는 div나 href가 있는 button처럼 의도와 HTML 요소가 어긋나는 상태를 기본 API로 만들기 어렵습니다.
그리고 인터페이스 자체가 이 경계를 설명합니다. 명령 메뉴의 동작이 필요하면 SectionItem에 키보드 이벤트를 덧붙이는 대신 DropdownMenuItem을 사용해야 합니다.
NavigationList: 이동과 현재 위치 소유
NavigationListItem은 nav 영역, URL 이동, 현재 페이지, 중첩 내비게이션을 표현합니다. leaf item은 링크 또는 버튼이 되고, 하위 항목을 가진 NavigationGroup은 aria-expanded와 aria-controls가 연결된 disclosure가 됩니다.
이 과정에서는 "부모 항목이 링크이면서 동시에 하위 목록을 여는 버튼이면 어떻게 할까?" 라는 반례가 중요했습니다. 하나의 클릭 영역이 이동과 펼치기라는 두 행동을 동시에 가지면 사용자도, 키보드도, AI도 의도를 해석하기 어려워집니다. 그래서 기본 모델에서는 leaf item이 이동을, branch item이 disclosure를 소유하게 했습니다. 부모 링크와 chevron을 분리하는 더 복잡한 형태는 실제 제품 요구가 분명해질 때 확장하기로 했습니다.
이제 사람과 AI는 "비슷하게 생긴 행을 만든 뒤 필요한 동작을 알아서 붙이는" 대신 먼저 이 항목의 목적을 선택합니다. 컴포넌트 이름이 맥락을 전달하고, 타입이 허용되는 문법을 제한하며, 접근성과 인터랙션은 디자인 시스템이 실행합니다.
좋은 추상화가 하네스가 되려면
이처럼 좋은 추상화는 인터페이스 하나로 완성되지 않습니다. 사람과 AI가 상황에 맞는 컴포넌트를 선택하도록 돕는 문서와 사용 예시, 구현이 약속을 지키는지 확인하는 테스트와 정적 분석이 함께 필요합니다.
다만 책임의 순서가 중요합니다. 컴포넌트가 먼저 의미와 접근성, 상호작용을 소유해야 합니다. 문서와 검증은 빠진 의미를 대신 채우는 것이 아니라, 추상화에 담긴 의도를 올바르게 선택하고 지키도록 돕습니다.
베지어를 개편하며 반복해서 확인한 기준은 네 가지였습니다.
1. 이름에서 목적이 보여야 합니다.
Item보다DropdownMenu,Navigation이 더 많은 맥락을 전달합니다.
2. 불가능한 상태는 타입으로 표현할 수 없어야 합니다.
서로 충돌하는 prop 조합을 열어둔 뒤 문서로 금지하기보다 union과 필수 값으로 닫습니다.
3. 접근성과 상호작용은 기본 경로에 포함되어야 합니다.
추가 설정 없이 role, 포커스, 키보드 동작이 따라와야 합니다.
4. 확장 지점은 좁고 책임이 분명해야 합니다.
기본 추상화로 해결할 수 없는 경우에만 탈출구를 열고, 이를 선택했을 때 사용자가 맡아야 할 책임을 설명합니다.
이 기준은 AI만을 위한 것이 아닙니다. 새로 합류한 개발자, 리뷰어, 테스트 코드, 스크린 리더 사용자도 같은 혜택을 얻습니다. 디자인 시스템은 원래 팀의 의도를 실행 가능한 계약으로 전달하는 소통 방식이었습니다. 지금은 그 계약을 읽고 사용하는 주체가 AI 에이전트까지 넓어졌고, 기존 추상화의 모호함이 더 빠르게 드러나고 있을 뿐입니다.
규칙을 추가하기 전에 책임의 위치를 묻기
AI가 같은 실수를 반복하면 프롬프트에 규칙을 한 줄 더 추가하고 싶어집니다. 그 방법이 가장 빠른 순간도 분명 있습니다. 다만 반복되는 규칙이라면 다음 질문을 먼저 해볼 가치가 있습니다.
이 규칙을 타입으로 닫을 수 있는가?
더 안전한 기본값으로 옮길 수 있는가?
접근성과 상호작용을 컴포넌트가 직접 소유할 수 있는가?
사용자가 목적을 선택하면 나머지가 자연스럽게 따라오도록 추상화를 나눌 수 있는가?
프롬프트와 스킬은 의도를 설명합니다. 좋은 추상화는 그 의도를 실행 가능한 계약으로 만듭니다.
AI가 디자인 시스템에 새로운 역할을 부여한 것은 아닙니다. 다만 이제 사람뿐 아니라 AI도 같은 인터페이스를 읽고 사용합니다. 오래전부터 중요했던 명확한 계약이 더 많은 실행을 좌우하게 됐습니다.
가장 좋은 하네스는 프롬프트보다 먼저, 추상화에서 시작합니다.
