browser
CSS 동작 원리: Specificity, BFC, Stacking Context, Flexbox, Grid, CSS-in-JS vs Tailwind
7년차 개발자가 z-index 버그를 Stacking Context 개념 없이 해결하려 하거나, float 레이아웃 문제를 BFC 이해 없이 접근하면 시니어답지 않습니다. 또한 CSS-in-JS vs Tailwind의 선택은 단순 취향이 아니라 성능 트레이드오프를 이해한 결정이어야 합니다.
학습 개요
탄생 배경
해결하려 했던 문제
HTML이 문서 구조를 담당하고, CSS가 분리되어 시각적 표현을 담당하는 관심사 분리(Separation of Concerns)가 필요했습니다. 초기 웹에서는 HTML 태그에 직접 color, font 속성을 넣었는데, 이것이 유지보수를 매우 어렵게 했습니다. CSS는 이를 분리하면서 캐스케이딩(상속과 우선순위)이라는 강력하지만 복잡한 메커니즘을 도입했습니다.
역사적 맥락
1996년 CSS1 표준화 → 1998년 CSS2 → 2004년부터 CSS3 모듈별 표준화 시작 → 2009년 Flexbox 초안 → 2011년 CSS Grid 초안 → 2015년 CSS Modules 등장 → 2016년 styled-components (CSS-in-JS) 등장 → 2017년 Tailwind CSS 등장 → 2019년 CSS Custom Properties (변수) 지원 광범위화 → 2023년 CSS 레이어(@layer), :has() 선택자 등 주요 기능 추가
이전에는 어떻게 했나
SASS/LESS 같은 CSS 전처리기가 변수, 중첩, 믹스인을 제공하여 CSS의 한계를 극복했습니다. BEM, SMACSS, OOCSS 같은 방법론으로 CSS 구조화를 시도했습니다.
멘탈 모델
동작 원리
[CSS 특이도(Specificity) 계산] 특이도는 (a, b, c, d) 4가지 가중치로 계산됩니다: - a: 인라인 스타일 (style="...") → 1,0,0,0 - b: ID 선택자 (#id) → 0,1,0,0 - c: 클래스(.class), 속성([attr]), 의사클래스(:hover) → 0,0,1,0 - d: 요소(div), 의사요소(::before) → 0,0,0,1 계산 예: #nav .item:hover → 0,1,1,1 .container > p → 0,0,1,1 !important는 특이도 계산을 초월 (사용 지양) [CSS Cascade (계단식 적용 규칙)] 여러 규칙이 충돌할 때 적용 우선순위: 1. !important (origin 고려) 2. 특이도(Specificity) 3. 코드 순서 (나중에 선언된 것이 적용) @layer로 선언 순서와 관계없이 레이어 우선순위를 명시적으로 제어 가능. [Block Formatting Context (BFC)] BFC는 독립적인 레이아웃 공간입니다. BFC 내부 요소는 외부와 독립적으로 배치됩니다. BFC 생성 조건: - display: flow-root (명시적 BFC 생성) - overflow: hidden/auto/scroll (auto 포함) - float: left/right - position: absolute/fixed - display: flex/grid (자식이 BFC) - display: inline-block BFC의 역할: 1. float 요소 포함: BFC는 내부 float를 높이 계산에 포함 (clearfix 불필요) 2. margin 겹침 방지: 다른 BFC의 margin은 겹치지 않음 3. float 요소 옆에 위치: BFC는 float 요소의 영역을 피함 [Stacking Context와 z-index] z-index는 Stacking Context(쌓임 맥락) 내에서만 비교됩니다. 다른 Stacking Context의 요소끼리는 z-index 값과 관계없이 Context 생성 순서로 결정. Stacking Context 생성 조건: - position: absolute/relative + z-index 숫자 값 - position: fixed/sticky - opacity < 1 - transform, filter, perspective 적용 - will-change: transform/opacity 등 - isolation: isolate z-index 버그의 원인: 부모가 Stacking Context를 생성하면, 자식의 z-index가 아무리 높아도 다른 Context의 요소보다 위에 올 수 없습니다. [Flexbox 내부 동작] Main Axis: flex-direction 방향 (기본 row → 가로) Cross Axis: Main Axis에 수직 flex: grow shrink basis - flex-grow: 남은 공간을 나누는 비율 - flex-shrink: 공간 부족 시 줄어드는 비율 - flex-basis: 초기 크기 (auto, 0, px, %) Flex 레이아웃 알고리즘: 1. flex-basis로 초기 크기 설정 2. 컨테이너 크기와 비교하여 여분 공간 계산 3. flex-grow/shrink 비율로 공간 분배 4. align-items로 Cross Axis 정렬 [CSS-in-JS vs CSS Modules vs Tailwind CSS] CSS-in-JS (styled-components, Emotion): 런타임: JavaScript가 실행될 때 스타일을 생성하고 style 태그에 삽입. SSR: 서버에서 Critical CSS를 추출하여 HTML에 인라인으로 삽입. 장점: 동적 스타일 (props에 따라 다른 스타일), 완전한 JS 통합. 단점: 런타임 비용, 번들 사이즈 증가, SSR에서 Critical CSS 추출 복잡성. 최신 변화: Linaria, vanilla-extract 등 Zero Runtime CSS-in-JS 등장. 빌드 타임에 CSS 파일로 추출하여 런타임 비용 제거. CSS Modules: 빌드 타임에 클래스명을 해시로 변환하여 스코프를 지역화. 런타임 비용 없음. 순수 CSS 파일 생성. 단점: 동적 스타일을 위해 추가 조작 필요. Tailwind CSS: 유틸리티 클래스의 모음. 디자인 시스템을 CSS로 구현. 빌드 타임에 사용된 클래스만 포함하여 최종 CSS 번들 최소화 (PurgeCSS). 런타임 비용 없음. 단점: 긴 className, 러닝커브, HTML 가독성 저하.
핵심 구성 요소
Specificity
CSS 선택자의 우선순위 계산. 충돌 시 적용될 스타일 결정
BFC (Block Formatting Context)
float 포함, margin 겹침 방지 등 독립적인 레이아웃 공간
Stacking Context
z-index가 비교되는 독립적인 레이어 공간. isolation: isolate로 생성
flex-grow/shrink/basis
Flexbox의 공간 분배 알고리즘을 제어하는 핵심 속성
CSS Custom Properties
CSS 변수. 런타임에 JavaScript로 변경 가능
@layer
CSS 규칙의 우선순위를 명시적으로 레이어별로 제어
흐름 설명
[z-index 버그 시나리오]
// HTML 구조
// div.modal (z-index: 9999)
// div.sidebar
// div.tooltip (z-index: 100)
// div.button (opacity: 0.9 → Stacking Context 생성!)
// 문제: tooltip이 modal 위에 표시됨
// 이유: button의 부모인 sidebar가 opacity로 Stacking Context 생성
// sidebar의 z-index가 modal보다 낮다면
// sidebar 내의 tooltip은 아무리 z-index가 높아도 modal 아래에 있음
// 해결: sidebar에서 opacity를 제거하거나 z-index를 조정
// 또는 modal을 별도 Stacking Context에 배치 (포털 사용)
[BFC를 이용한 float 포함]
/* 문제: 자식이 float이면 부모 높이가 0이 됨 */
.parent { /* height: 0 */ }
.child { float: left; height: 100px; }
/* 해결 1: overflow: hidden으로 BFC 생성 */
.parent { overflow: hidden; }
/* 해결 2: display: flow-root (명시적) */
.parent { display: flow-root; }
/* 구식 방법: clearfix */
.parent::after {
content: '';
display: block;
clear: both;
}
코드 예제
/* CSS Specificity 계산 예시 */
/* 0,1,0,0 = 100 */
#header { color: blue; }
/* 0,0,2,1 = 21 */
.nav .item a { color: red; }
/* ID가 이김 → blue */
/* Stacking Context 격리 */
.modal-container {
isolation: isolate; /* 새 Stacking Context 생성 */
position: fixed;
z-index: 1000;
}
/* Flexbox 공간 분배 */
.flex-container {
display: flex;
width: 300px;
}
.flex-item-1 { flex: 1 1 100px; } /* grow:1, shrink:1, basis:100px */
.flex-item-2 { flex: 2 1 100px; } /* grow:2, shrink:1, basis:100px */
/* 남은 공간 100px를 1:2로 분배 → item-1: 133px, item-2: 167px */
/* CSS Custom Properties + JS 동적 테마 */
:root {
--color-primary: #0070f3;
--spacing-unit: 8px;
}
.button {
background: var(--color-primary);
padding: var(--spacing-unit);
}
/* JS에서 런타임 변경 */
// document.documentElement.style.setProperty('--color-primary', '#ff0000');
/* Tailwind CSS - 유틸리티 조합 */
/* <div class="flex items-center justify-between p-4 bg-white shadow-md rounded-lg"> */
/* CSS Modules - 스코프 지역화 */
/* Button.module.css */
/* .button { background: blue; } */
/* 빌드 후: .Button_button__a1b2c { background: blue; } */
import styles from './Button.module.css';
function Button() {
return <button className={styles.button}>클릭</button>;
}
/* styled-components - 동적 스타일 */
const Button = styled.button<{ primary?: boolean }>`
background: ${props => props.primary ? 'blue' : 'white'};
color: ${props => props.primary ? 'white' : 'blue'};
`;
비교 분석
CSS
vsCSS-in-JS (styled-components/Emotion)
CSS
vsFlexbox vs Grid
트레이드오프
이상적인 사용 사례