스타일 토큰 우회

스타일 토큰 우회

이 사례의 핵심은 “스타일을 통일하자"가 아니라
토큰을 거치지 않으면 시각 표현을 만들 수 없게 만드는 것이다.

개요

렌더링 경로, 컴포넌트 조합, 상태 바인딩을 모두 고정해도
UI는 여전히 무너질 수 있다.

그 이유는 표현 단계에서 다음 우회가 가능하기 때문이다.

  • 색상 직접 입력
  • spacing 직접 숫자 입력
  • 폰트, radius, shadow를 임의 값으로 선언
  • 컴포넌트 외부에서 스타일을 덮어씀

즉 문제는 UI가 무엇을 그리는가가 아니라
어떤 시각 규칙으로 그리는가가 구조 밖으로 새는 것이다.

Glif에서는 다음을 목표로 한다.

  • 시각 표현은 theme/token 시스템을 통해서만 정의된다
  • 하드코딩된 스타일 값은 허용되지 않는다
  • 위반은 코드 리뷰가 아니라 정적 분석 또는 런타임에서 실패한다

이 문서의 질문

이 문서는 하네스 개요에서 정리한 다섯 공통 패턴 중 parallel path divergence에 가장 가깝다. 핵심 질문은 다음과 같다.

시각 표현이 token과 theme를 거친 공식 경로로만 만들어지는가

문제

다음과 같은 코드가 반복적으로 생성된다.

  • style={{ color: "#ff0000" }}
  • margin: 13px
  • border-radius: 7px
  • font-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에서는 시각 표현 경로를 다음처럼 고정한다.

D2 diagram
핵심 규칙은 단순하다.
  • 색상, spacing, radius, typography는 token으로만 표현된다

  • 컴포넌트는 token 또는 variant만 받는다

  • literal style 값은 구조적으로 금지된다

하네스 적용 위치

이 문제는 네 레벨에서 동시에 막는다.

구조 레벨

스타일 진입점을 줄인다.

  • 공통 style API 또는 variant API만 허용

  • 컴포넌트 외부 ad-hoc class 주입 제한

  • inline style 사용 금지

  • layout과 visual token 책임 분리

즉 UI는 스타일 값을 “작성"하는 곳이 아니라
토큰을 선택하는 곳으로 고정한다.

검증 레벨

정적 분석으로 literal style을 탐지한다.

  • JSX style prop 금지

  • 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이라는 우회 경로는 제거된다

“스타일을 통일하라"가 아니라
“토큰을 거치지 않으면 시각 표현을 만들 수 없다"가 하네스다.