레이아웃 우회
이 사례의 핵심은 “레이아웃을 잘 잡자"가 아니라
레이아웃 시스템을 거치지 않으면 배치를 만들 수 없게 만드는 것이다.
개요
스타일 토큰까지 통제해도 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에서는 배치 경로를 다음처럼 고정한다.
위치는 레이아웃 컴포넌트로만 결정된다
spacing은 토큰 기반 layout API로만 조정된다
absolute positioning은 기본적으로 금지된다
하네스 적용 위치
이 문제는 네 레벨에서 동시에 막는다.
구조 레벨
레이아웃 책임을 전용 컴포넌트로 고정한다.
Stack,Inline,Grid,Box같은 layout primitives 제공컴포넌트는 layout을 직접 가지지 않음
위치 결정은 부모 layout이 담당
즉 컴포넌트는 배치하는 것이 아니라
배치되는 대상이 된다.
검증 레벨
정적 분석으로 layout 우회를 탐지한다.
position: absolute금지top,left,right,bottom사용 제한z-indexliteral 금지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가 유지되며
좌표 기반 배치라는 우회 경로는 제거된다
즉
“레이아웃을 잘 구성하라"가 아니라
“레이아웃 시스템을 거치지 않으면 배치를 만들 수 없다"가 하네스다.