레이아웃 우회

레이아웃 우회

이 사례의 핵심은 “레이아웃을 잘 잡자"가 아니라
레이아웃 시스템을 거치지 않으면 배치를 만들 수 없게 만드는 것이다.

개요

스타일 토큰까지 통제해도 UI는 여전히 무너질 수 있다.

그 이유는 레이아웃이 다음과 같은 방식으로 우회될 수 있기 때문이다.

  • position: absolute로 위치 강제
  • top, left로 임의 배치
  • z-index로 레이어 충돌 해결
  • margin/padding으로 구조를 임시 보정
  • 레이아웃 컴포넌트를 건너뛰고 직접 배치

즉 문제는 배치 결과가 아니라
배치 방식이 구조 밖으로 새는 것이다.

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

  • 모든 배치는 레이아웃 시스템을 통해서만 수행된다
  • absolute/overlay 같은 escape hatch는 제한된 경로로만 사용된다
  • 위반은 코드 리뷰가 아니라 정적 분석 또는 런타임에서 실패한다

이 문서의 질문

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

배치가 layout system을 거친 공식 경로로만 만들어지는가

문제

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

<div style={{ position: "absolute", top: 7, left: 13 }}>
  Button
</div>

또는

<div style={{ marginTop: "13px", marginLeft: "9px" }}>
  Content
</div>

또는

<div style={{ zIndex: 9999 }}>
  Modal
</div>

이 방식은 빠르게 화면을 맞춘다.
하지만 구조적으로는 다음 문제를 만든다.

  • 레이아웃 규칙이 깨짐

  • 반응형 동작 불가능

  • 요소 간 관계가 사라짐

  • 레이어 충돌 발생

  • 동일 구조 UI가 서로 다른 배치 방식으로 구현됨

즉 문제는 위치 조정이 아니라
레이아웃 시스템을 우회하는 것이다.

문제의 본질

사람과 AI는 레이아웃 구조를 이해하기보다
현재 보이는 결과를 맞추는 쪽으로 움직인다.

기준은 단순하다.

  • 위치만 맞으면 된다

  • 부모 구조를 이해할 필요가 없다

  • 기존 레이아웃 컴포넌트를 탐색하지 않아도 된다

그래서 다음과 같은 우회가 발생한다.

  • absolute positioning으로 빠르게 맞춤

  • margin으로 틈을 메움

  • z-index로 충돌 해결

  • 임시 wrapper 추가

즉 사람과 AI는 레이아웃 시스템을 따르기보다
좌표 기반 배치로 문제를 해결한다.

목표 구조

Glif에서는 배치 경로를 다음처럼 고정한다.

D2 diagram
핵심 규칙은 단순하다.
  • 위치는 레이아웃 컴포넌트로만 결정된다

  • spacing은 토큰 기반 layout API로만 조정된다

  • absolute positioning은 기본적으로 금지된다

하네스 적용 위치

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

구조 레벨

레이아웃 책임을 전용 컴포넌트로 고정한다.

  • Stack, Inline, Grid, Box 같은 layout primitives 제공

  • 컴포넌트는 layout을 직접 가지지 않음

  • 위치 결정은 부모 layout이 담당

즉 컴포넌트는 배치하는 것이 아니라
배치되는 대상이 된다.

검증 레벨

정적 분석으로 layout 우회를 탐지한다.

  • position: absolute 금지

  • top, left, right, bottom 사용 제한

  • z-index literal 금지

  • margin/padding literal 제한

실행 레벨

런타임에서도 layout 위반을 감지한다.

  • forbidden style 사용 시 dev-mode 에러

  • layout context 밖 positioning 감지

  • overlay misuse 탐지

테스트 레벨

레이아웃 구조도 검증 대상이다.

  • layout snapshot 검증

  • storybook에서 다양한 viewport 테스트

  • 임의 배치 없는 구조만 fixture로 제공

실제 구현

배치 권한을 layout primitive로 모은다

layout bypass의 핵심은 각 컴포넌트가 좌표와 간격을 직접 정하지 못하게 만드는 것이다.

type StackProps = {
  direction?: "vertical" | "horizontal";
  gap?: keyof typeof tokens.spacing;
  children: React.ReactNode;
};

export function Stack({ direction = "vertical", gap = 2, children }: StackProps) {
  return (
    <div data-layout="stack" data-direction={direction}>
      {children}
    </div>
  );
}

이 순간부터 spacing과 배치는 child component가 아니라 Stack, Grid, Inline 같은 primitive의 책임이 된다.

absolute와 z-index 같은 raw escape hatch를 닫는다

position: absolute, zIndex: 9999, 임의 margin 보정은 대부분 구조 문제를 값으로 봉합하는 경로다. 그래서 이런 속성은 raw style로 열어두기보다 정적 분석으로 막고, 꼭 필요한 경우만 승인된 surface로 승격시키는 편이 맞다.

예외는 Layer 같은 이름 있는 surface 뒤로 숨긴다

type LayerLevel = "base" | "dropdown" | "modal";

export function Layer({ level, children }: { level: LayerLevel; children: React.ReactNode }) {
  return <div data-layer={level}>{children}</div>;
}

핵심은 escape hatch를 없애는 게 아니라, 이름 없는 값 선택을 이름 있는 의미 surface로 바꾸는 것이다.

테스트도 layout primitive를 기준으로 작성한다

테스트가 margin과 absolute positioning을 허용한 채 스냅샷만 맞추기 시작하면 실제 코드도 같은 지름길을 배우게 된다. fixture와 helper도 Stack, Grid, Layer 같은 primitive 경로를 반복해야 한다.

위반 예시와 실패 결과

다음 코드는 반드시 실패해야 한다.

<div style={{ position: "absolute", top: 10 }}>
  Button
</div>

실패 결과:

error: Absolute positioning is forbidden. Use layout primitives.

또는

<div style={{ marginTop: "13px" }}>
  Content
</div>

실패 결과:

error: Use layout gap instead of margin

또는

<div style={{ zIndex: 9999 }}>
  Modal
</div>

실패 결과:

error: Use Layer component instead of z-index

이 순간부터 레이아웃 규칙은 문서가 아니라
실행 가능한 제약이 된다.

보조 강제 수단

이 케이스의 보조 강제 수단은 배치 권한을 layout primitive와 제한된 escape hatch로 모으는 데 있다. lint만 두면 합법 primitive가 부족해지고, primitive만 두면 raw CSS escape가 남고, Layer 같은 공식 예외 surface가 없으면 우회가 다시 퍼진다.

  • ESLint → absolute, z-index, margin literal 차단
  • layout primitives → 배치 경로 고정
  • Layer component → escape hatch 제한
  • runtime guard → style 위반 감지
  • test fixture → 구조 반복 검증

이 다섯 겹은 “위치만 맞추면 된다"는 충동을 공식 layout language 안으로 되돌린다.

결과

하네스 적용 이후 변화는 명확하다.

  • 배치는 layout primitive를 거친 공식 경로로 다시 수렴한다.
  • absolute, z-index 같은 강한 escape hatch는 명시적이고 제한된 경우에만 남는다.
  • spacing과 alignment 규칙이 화면마다 달라지지 않고 재사용 가능해진다.

실무 적용 팁

absolute는 대부분 구조 문제의 신호다

허용하면 항상 빠른 해결책으로 선택된다.

layout primitive를 충분히 제공해야 한다

Stack, Grid, Inline 등 기본 레이아웃이 부족하면
우회가 자연스럽게 발생한다.

spacing은 layout 책임으로 둔다

컴포넌트가 spacing을 가지면
구조는 금방 무너진다.

fallback으로 복구하지 않는다

레이아웃 오류를 자동 보정하면
우회는 정상 경로로 학습된다.

요약

이 케이스에서 하네스는 다음과 같이 구성된다.

  • absolute positioning과 z-index는 정적 분석으로 차단된다

  • 배치는 layout primitive를 통해서만 수행된다

  • escape hatch는 Layer 컴포넌트로 제한된다

  • 런타임에서도 style 위반이 감지된다

  • 테스트도 layout 기반 fixture만 허용한다

결과적으로

  • 배치 경로는 layout 시스템으로 고정되고

  • 구조 기반 UI가 유지되며

  • 좌표 기반 배치라는 우회 경로는 제거된다

“레이아웃을 잘 구성하라"가 아니라
“레이아웃 시스템을 거치지 않으면 배치를 만들 수 없다"가 하네스다.