37signals의 CSS 아키텍처는 놀랍도록 단순합니다. app/assets/stylesheets/ 아래에 _reset.css, base.css, colors.css, utilities.css, buttons.css, inputs.css 등 개념별로 하나의 파일이 존재하며, 서브디렉토리나 복잡한 임포트 트리가 없습니다. 이는 제로 설정, 제로 빌드 시간, 제로 대기 시간을 목표로 합니다.
색상 시스템: 일관된 기반, 진화하는 기능
모든 애플리케이션은 지각적으로 균일한 색상 공간인 OKLCH를 사용합니다. 루트 변수로 정의된 LCH 값(--lch-blue, --lch-red, --lch-green)을 기반으로 --color-link, --color-negative와 같은 시맨틱 색상을 정의하며, 다크 모드는 @media (prefers-color-scheme: dark) 내에서 LCH 값을 조정하는 것만으로 쉽게 구현됩니다. Fizzy에서는 color-mix()를 활용하여 --card-color 변수 하나로 --card-bg, --card-text, --card-border와 같은 조화로운 색상 팔레트를 동적으로 생성합니다.
간격 시스템: 픽셀 대신 문자(ch) 단위
수평 간격에 ch 단위를 사용하는 것이 특징입니다. 1ch는 문자 하나의 너비에 해당하여 콘텐츠와 자연스럽게 연관됩니다. 이는 폰트 크기 조정 시 간격도 비례하여 조정되게 하며, 반응형 브레이크포인트도 (min-width: 100ch)와 같이 콘텐츠 기반의 의미론적 접근을 가능하게 합니다.
유틸리티 클래스: 보조적 역할
37signals 애플리케이션도 .flex, .gap, .pad와 같은 유틸리티 클래스를 사용하지만, Tailwind와 달리 이러한 유틸리티는 핵심 스타일링이 아닌 예외적인 상황이나 일회성 조정을 위한 보조적인 역할을 합니다. 핵심 스타일은 시맨틱 컴포넌트 클래스(btn)에 정의되어 HTML 가독성을 높이고, 변경 사항이 쉽게 전파되며, 미디어 쿼리 및 호버 상태가 컴포넌트와 함께 관리됩니다.
:has() 혁명
:has() 선택자는 JavaScript 없이 부모 요소를 자식 요소의 상태에 따라 스타일링할 수 있게 하여, 사이드바 토글, 칸반 컬럼 레이아웃 조정, 아이콘 버튼 스타일링 등 복잡한 상태 관리와 조건부 렌더링을 CSS만으로 구현합니다.
아키텍처의 점진적 진화
-
Campfire: OKLCH 색상, 커스텀 속성, 문자 기반 간격, 플랫 파일 조직, View Transitions API를 확립했습니다.
-
Writebook: 컴포넌트 수준 반응형을 위한 Container Queries, 진입 애니메이션을 위한
@starting-style을 추가했습니다. -
Fizzy: 특수성 관리를 위한 CSS Layers (
@layer), 동적 색상 파생을 위한color-mix(), JavaScript 상태를 대체하는 복잡한:has()체인을 도입하여 현대 CSS 기능을 최대한 활용했습니다.
특별한 기술들
-
로딩 스피너: 이미지나 SVG, JavaScript 없이 CSS 마스크와
@keyframes애니메이션으로 세 개의 점이 순차적으로 움직이는 스피너를 구현합니다.currentColor를 사용하여 부모의 텍스트 색상을 자동으로 상속받습니다. -
향상된
<mark>: 기본<mark>대신border-radius를 비대칭적으로 사용하여 손으로 그린 듯한 원형 하이라이트를 구현합니다.mix-blend-mode를 통해 배경과의 자연스러운 블렌딩을 제공합니다. -
Dialog 애니메이션:
@starting-style과allow-discrete를 활용하여 HTML<dialog>요소의 부드러운 진입 및 종료 애니메이션을 순수 CSS로 구현합니다. 이는display: none과display: block간의 전환을 애니메이션할 수 있게 합니다.