스타일 토큰 우회
이 사례의 핵심은 “스타일을 통일하자"가 아니라
토큰을 거치지 않으면 시각 표현을 만들 수 없게 만드는 것이다.
개요
렌더링 경로, 컴포넌트 조합, 상태 바인딩을 모두 고정해도
UI는 여전히 무너질 수 있다.
그 이유는 표현 단계에서 다음 우회가 가능하기 때문이다.
- 색상 직접 입력
- spacing 직접 숫자 입력
- 폰트, radius, shadow를 임의 값으로 선언
- 컴포넌트 외부에서 스타일을 덮어씀
즉 문제는 UI가 무엇을 그리는가가 아니라
어떤 시각 규칙으로 그리는가가 구조 밖으로 새는 것이다.
Glif에서는 다음을 목표로 한다.
- 시각 표현은 theme/token 시스템을 통해서만 정의된다
- 하드코딩된 스타일 값은 허용되지 않는다
- 위반은 코드 리뷰가 아니라 정적 분석 또는 런타임에서 실패한다
이 문서의 질문
이 문서는 하네스 개요에서 정리한 다섯 공통 패턴 중 parallel path divergence에 가장 가깝다.
핵심 질문은 다음과 같다.
시각 표현이 token과 theme를 거친 공식 경로로만 만들어지는가
문제
다음과 같은 코드가 반복적으로 생성된다.
style={{ color: "#ff0000" }}margin: 13pxborder-radius: 7pxfont-size: 15px- 임의의
z-index,position: absolute사용 - 기존 컴포넌트 위에 ad-hoc CSS class 덧씌우기
예를 들면 다음과 같다.
function DangerText() {
return (
<div style={{ color: "#ff0000", fontSize: "15px", marginTop: "13px" }}>
Danger
</div>
);
}또는
const buttonStyle = {
borderRadius: "7px",
padding: "5px 11px",
boxShadow: "0 0 3px rgba(0,0,0,0.4)",
};이 방식은 빠르게 결과를 만든다.
하지만 구조적으로는 다음 문제를 만든다.
색상 체계가 파편화됨
spacing rhythm이 깨짐
다크/라이트 모드 일관성 붕괴
동일 역할 UI가 서로 다른 인상을 가짐
디자인 시스템이 선택 사항으로 전락함
즉 문제는 스타일 선언 자체가 아니라
스타일 값이 토큰 체계를 통과하지 않는 것이다.
문제의 본질
사람과 AI는 시각 규칙보다
현재 필요한 결과를 가장 빨리 만드는 쪽으로 움직인다.
기준은 단순하다.
눈앞의 화면을 맞춘다
토큰 이름을 찾는 비용을 생략한다
기존 theme 구조를 이해하지 않아도 된다
그래서 다음과 같은 우회가 쉽게 발생한다.
hex color 직접 입력
spacing 숫자 직접 입력
특정 화면만 맞추는 absolute tweak 추가
기존 컴포넌트 토큰을 무시한 ad-hoc override 작성
즉 사람과 AI는 디자인 시스템을 보존하기보다
즉시 맞아 보이는 값을 선택한다.
목표 구조
Glif에서는 시각 표현 경로를 다음처럼 고정한다.
색상, spacing, radius, typography는 token으로만 표현된다
컴포넌트는 token 또는 variant만 받는다
literal style 값은 구조적으로 금지된다
하네스 적용 위치
이 문제는 네 레벨에서 동시에 막는다.
구조 레벨
스타일 진입점을 줄인다.
공통 style API 또는 variant API만 허용
컴포넌트 외부 ad-hoc class 주입 제한
inline style 사용 금지
layout과 visual token 책임 분리
즉 UI는 스타일 값을 “작성"하는 곳이 아니라
토큰을 선택하는 곳으로 고정한다.
검증 레벨
정적 분석으로 literal style을 탐지한다.
JSX
styleprop 금지hex/rgb literal 금지
px/rem 숫자 literal 제한
임의 className 문자열 금지 또는 제한
position: absolute,z-index같은 escape hatch 제한
실행 레벨
런타임에서도 theme 경계를 확인한다.
ThemeProvider 밖에서 style hook 사용 시 throw
token lookup 실패 시 fallback 없이 실패
개발 모드에서 unknown token 사용 감지
테스트 레벨
시각 회귀도 하네스에 포함한다.
token 기반 snapshot만 허용
storybook에서 variant matrix 검증
literal style이 들어간 story 금지
실제 구현
시각 값은 token 이름 뒤에 숨기고 직접 값은 닫는다
style bypass의 핵심은 컴포넌트가 hex, px, z-index 같은 값을 직접 쓰지 못하게 만드는 것이다.
export const tokens = {
color: {
text: {
primary: "var(--color-text-primary)",
danger: "var(--color-text-danger)",
},
},
spacing: {
1: "4px",
2: "8px",
3: "12px",
4: "16px",
},
} as const;이제 UI는 값을 쓰는 대신 토큰 이름을 고른다.
style prop과 literal value를 같이 막는다
inline style만 막으면 class literal이 남고, hex만 막으면 px와 z-index가 남는다. 그래서 style={{ ... }}와 literal style value를 함께 차단하는 편이 좋다.
표현 surface는 variant API로 고정한다
type TextProps = {
tone?: "primary" | "danger";
size?: "body" | "caption";
children: React.ReactNode;
};
export function Text({ tone = "primary", size = "body", children }: TextProps) {
return <span className={textVariants({ tone, size })}>{children}</span>;
}허용되는 자유도는 tone="danger", size="caption" 같은 variant 선택뿐이고, raw style 값은 surface 밖으로 밀려난다.
theme 경계 밖 token 접근도 실패시킨다
const ThemeContext = React.createContext<typeof tokens | null>(null);
export function useThemeTokens() {
const value = React.useContext(ThemeContext);
if (!value) {
throw new Error("useThemeTokens must be used within ThemeProvider");
}
return value;
}없는 토큰을 비슷한 값으로 조용히 복구하지 않는 것도 중요하다. fallback은 시각 규칙 위반을 정상처럼 위장한다.
escape hatch도 별도 컴포넌트 뒤로 숨긴다
zIndex: 9999, position: absolute, 임의 top/left 보정 같은 값이 꼭 필요하다면 raw style이 아니라 승인된 component surface 뒤로 넣는 편이 낫다.
story와 테스트도 token/variant만 반복하게 만든다
Storybook과 테스트가 inline style을 허용하면 실제 코드도 가장 짧은 우회 경로를 다시 배우게 된다.
위반 예시와 실패 결과
다음 코드는 반드시 실패해야 한다.
<Text style={{ color: "#ff0000" }}>Danger</Text>실패 결과:
error: style prop is forbidden. Use variants or theme tokens.또는
<div className="text-[#ff0000] mt-[13px]">Danger</div>실패 결과:
error: Use theme tokens instead of literal style values또는 Provider 밖 token 사용:
Error: useThemeTokens must be used within ThemeProvider이 순간부터 시각 일관성은 문서가 아니라
실행 가능한 제약이 된다.
보조 강제 수단
이 케이스의 보조 강제 수단은 visual choice를 token과 variant surface로 다시 모으는 데 있다.
lint만 두면 표현력이 부족해지고, token object만 두면 inline style escape가 남고, 공식 예외 컴포넌트가 없으면 개발자가 raw style로 되돌아간다.
- ESLint → inline style, literal value 차단
- token object / variant API → 표현 경로 고정
- ThemeProvider guard → 런타임 경계 확인
- storybook / snapshot → 허용된 표현 반복 검증
- escape hatch component → 예외 경로 축소
이 다섯 겹은 시각 표현을 감각적 임의 선택이 아니라 공유된 언어로 유지한다.
결과
하네스 적용 이후 변화는 명확하다.
- 색상, 간격, radius 같은 시각 선택이 token과 variant 경로로 다시 모인다.
- literal style 값은 허용된 escape hatch가 아니면 즉시 실패한다.
- theme 변경과 시각 규칙 수정이 화면 전반에 예측 가능하게 전파된다.
실무 적용 팁
style prop은 거의 항상 우회 경로다
허용하는 순간
디자인 시스템은 선택 사항이 된다.
토큰 이름을 찾기 쉽게 만들어야 한다
하네스는 금지만으로 끝나지 않는다.
짧은 우회를 막는 대신
합법 경로를 더 찾기 쉽게 만들어야 한다.
variant가 충분히 표현력을 가져야 한다
variant가 빈약하면
AI와 사람 모두 결국 literal style로 도망간다.
fallback으로 복구하지 않는다
토큰을 못 찾았을 때 기본값을 조용히 넣어주면
우회는 계속 살아남는다.
요약
이 케이스에서 하네스는 다음과 같이 구성된다.
inline style과 literal style 값은 정적 분석으로 차단된다
시각 표현은 token과 variant API를 통해서만 정의된다
ThemeProvider 밖 token 접근은 런타임에서 즉시 실패한다
escape hatch 속성은 별도 컴포넌트 뒤로 숨겨진다
storybook과 테스트도 token 기반 표현만 허용한다
결과적으로
시각 표현 경로는 theme 중심으로 고정되고
디자인 시스템은 선택이 아니라 구조가 되며
ad-hoc styling이라는 우회 경로는 제거된다
즉
“스타일을 통일하라"가 아니라
“토큰을 거치지 않으면 시각 표현을 만들 수 없다"가 하네스다.