목차10개 항목
출처와 참고
테오(teo)의 「CSS 역사로 알아보는 CSS가 어려워진 이유」(2022.01)를 바탕으로 재구성한 노트다.
CSS는 왜 어려워졌나
CSS 문법 자체는 어렵지 않다. 어려운 것은 무엇이 좋은 CSS인지 알기 어렵다는 것이다. 원인은 둘이다.
- 태생의 문제. CSS는 문서의 서식과 내용을 분리하려고 만들었다. 웹이 문서에서 애플리케이션으로 바뀌는 동안, 문서용 설계로 앱을 만들려고 스펙을 원래 의도와 다르게 빌려 썼다. 그 우회법이 팁이 되고, 팁이 정석이 되어 강의와 문서로 재생산됐다.
- 도구의 범람. 이 문제를 풀려는 새 스펙과 도구가 계속 나왔다. 더 나은 방법이 나오면 이전 방법은 나쁜 방법이 되지만, 옛 자료는 남아 있다. 그렇다고 최신 스펙을 바로 쓸 수도 없다. 모든 브라우저가 지원하기까지 시간이 걸린다.
그래서 적당한 기준으로, 적당히 좋은 도구를, 적당한 시기에 꾸준히 갱신하는 것이 CSS를 잘하는 길이다. 역사를 알면 지금 읽는 자료가 어느 시대, 어느 갈래의 이야기인지 구분할 수 있다.
두 가지 구조적 문제
| 문제 | 설명 |
|---|---|
| 전역 범위(Global Scope) | 모든 규칙이 문서 전체에 적용된다. 문서 하나를 일괄로 꾸밀 때는 장점이었지만, 컴포넌트 단위 개발에서는 다른 컴포넌트의 스타일을 건드릴 수 있다는 약점이 된다 |
| 명시도(Specificity) | 규칙마다 고유한 우선순위가 있어 작성 순서와 상관없이 적용된다. 덮어쓰려면 더 복잡한 선택자를 쓰고, 그걸 또 덮으려면 더 복잡하게, 결국 !important를 남발하는 명시도 전쟁이 생긴다 |
캐스케이드(Cascade)는 원래 "단순한 서식을 먼저 칠하고 구체적인 것을 덧칠한다"는 좋은 의도였다. 전역 범위와 함께 명시도는 대규모 CSS를 유지하기 어렵게 만드는 핵심 원인이다. 이후 등장한 방법론, CSS Modules, CSS-in-JS, 유틸리티 CSS는 모두 이 두 문제를 피하려는 시도로 읽을 수 있다. 명시도 계산법은 선택자와 우선순위에 있다.
연대표
timeline 1990 : HTML 1996 : CSS 2003 : CSS Zen Garden 2006 : Sass 2008 : jQuery·Ajax 2013 : React·BEM·Flexbox 2014 : Zeplin 2015 : IE11 기능 업데이트 중단·CSS Modules 2016 : styled-components 2017 : Tailwind·CSS 변수·Grid 2020 : Figma 1위 2021~ : 온디맨드 Atomic CSS 2022 : IE11 지원 종료 2022~23 : @layer·컨테이너 쿼리·네이티브 중첩·#58;has() 2025 : Tailwind v4·@scope 2026 : 앵커 위치 지정
문서의 시대: 인라인 스타일에서 시맨틱까지
| 단계 | 일어난 일 | 남긴 것 |
|---|---|---|
| 인라인 스타일 | 태그마다 style을 직접 쓴다. 색 하나 바꾸려면 전부 찾아 고쳐야 한다 | 반복 제거의 필요 |
| CSS 탄생 | 스타일을 선언(declaration) 으로 묶고 선택자(selector) 로 대상을 고른다. 반복 입력을 대신하는 매크로에 가깝다 | 선택자 + 선언 = 규칙(Ruleset) |
| 솔루션 시대 | 게시판·쇼핑몰은 백엔드 솔루션이 HTML을 만들었다. HTML을 못 고치니 CSS만으로 디자인해야 했다 | 점점 복잡해지는 선택자, 높아지는 명시도 |
| CSS Zen Garden (2003) | 같은 시맨틱 HTML에 CSS만 바꿔 전혀 다른 디자인을 보여 줬다 | 시맨틱 패러다임. 클래스 이름은 모양(.red)이 아니라 의미(.error) 로 짓는다 |
| Sass (2006) | 중첩 선택자와 변수를 쓰고 CSS로 변환하는 전처리기(pre-processor). LESS·Stylus가 뒤따랐지만 Sass가 1위를 지켰다 | CSS에 로직과 재사용을 들이려는 첫 시도. 실제 재사용은 구조상 어려웠다 |
시맨틱 패러다임은 지금도 워드프레스 같은 테마형 사이트에서 유효하고 웹의 다수를 차지한다. 하지만 하나의 디자인 시스템으로 컴포넌트를 키우는 현대 앱 개발과는 맞지 않는 부분이 있다. 두 세계를 구분해서 읽어야 한다.
앱의 시대: 프론트엔드가 HTML을 갖게 되다
Ajax와 jQuery 이후 백엔드는 JSON만 주고, HTML과 CSS는 프론트엔드가 함께 쓰게 됐다. HTML을 직접 고칠 수 있으니 복잡한 선택자 대신 클래스를 추가하는 단순한 선택자로 방향이 바뀌었다. React 같은 프레임워크가 컴포넌트 단위 개발을 굳히자, 문서를 꾸미려고 만든 CSS로 앱을 만드는 진통이 본격화됐다. "요소를 가운데 정렬하는 7가지 방법" 같은 글이 인기였던 시절이다.
| 해법 | 무엇을 풀었나 |
|---|---|
| CSS 방법론과 BEM (2013 무렵) | 여럿이 함께 쓸 아키텍처, 명시도 관리, 이름 짓기 규칙. OOCSS·SMACSS·ITCSS 중 BEM(.block__element--modifier)이 살아남았다. 구조를 표현하면서도 종속이 없고, 명시도를 클래스 하나로 통일하고, 규칙이 단순하다 |
| Flexbox (2013) | 문서용 CSS와 앱 레이아웃용 스펙이 처음 분리됐다. float·table·absolute로 억지로 만들던 레이아웃 우회법은 이제 배울 필요가 없다. 자세한 내용은 Float와 Flexbox |
| 핸드오프 도구 (Zeplin 2014, Figma) | 디자이너가 퍼블리싱까지 하던 구조가 끝나고, 디자인을 컴포넌트 구조로 옮기는 능력이 개발자의 몫이 됐다 |
| CSS Modules (2015) | 컴포넌트에서 불러온 CSS를 그 컴포넌트에만 적용되게 컴파일한다. 전역 범위 문제를 JS 쪽 도구로 풀었다 |
| CSS-in-JS (styled-components 2016) | 스타일을 JS로 만들어 선택자 자체를 없앴다. 명시도의 근원인 선택자가 사라졌다. 런타임에 CSS를 만드는 비용을 줄이려 zero-runtime 방식이 이어졌다 |
| 유틸리티 CSS (Tailwind 2017) | .mt10 같은 유틸리티 클래스는 한때 안티패턴이었다. Tailwind는 이를 전면적으로 권하며 패러다임을 뒤집었다 |
| CSS 변수, Grid (2017) | 변수는 Sass를 쓸 이유를 줄였다. Grid는 IE11 때문에 한동안 흥행하지 못했다 |
유틸리티 CSS가 받아들여진 이유
- 이제는 솔루션이 아니라 프론트엔드가 HTML과 CSS를 직접 고친다.
- CSS 파일을 고치는 것보다 HTML을 다루는 편이 쉽고 부작용이 적다.
- 서비스 대부분이 디자인 시스템 하나를 쓰므로 스타일시트를 갈아 끼워 테마를 바꿀 일이 없다.
- 모든 클래스의 명시도가 같고, 이름을 지을 필요가 없고, 서비스가 커져도 CSS 양이 늘지 않는다.
미리 만든 클래스로는 임의의 수치·색을 표현하기 어렵다는 한계에서, 클래스 이름을 읽어 필요한 CSS만 빌드 시점에 생성하는 온디맨드(on-demand) Atomic CSS(UnoCSS, Tailwind 3 이후의 JIT 엔진 등)로 이어졌다.
IE11이라는 방파제
IE11은 2015년 이후 기능 업데이트가 멈췄지만 점유율이 높아, 새 스펙을 써도 IE11에서 안 되니 쓰기 어려웠다. CSS의 문제를 CSS가 아니라 JS 도구로 풀려는 흐름이 커진 이유 중 하나다. 2022년 6월 지원이 끝나자 그동안 쌓인 스펙을 한꺼번에 쓸 수 있게 됐다.
CSS가 스스로 푸는 시대
2022년 이후 주요 브라우저가 함께 들여온 스펙은, 그동안 도구로 메우던 구조적 문제를 CSS 안에서 직접 다룬다. 각 스펙의 Baseline 연도와 지금 써도 되는지는 CSS 점진적 향상에 있다.
| 스펙 | 대신하는 것 |
|---|---|
캐스케이드 레이어 @layer | 레이어 순서로 우선순위를 정해 명시도 전쟁을 끝낸다. 리셋·라이브러리·컴포넌트·유틸리티를 층으로 나눈다 |
@scope | 스타일이 적용될 범위를 DOM 하위 트리로 제한한다. 전역 범위 문제를 BEM이나 CSS Modules 없이 다룬다 |
| 네이티브 중첩(nesting) | Sass의 중첩 문법을 CSS가 직접 지원한다 |
color-mix() | 전처리기의 색 계산(darken()·mix())을 대신한다. 변수는 앞의 CSS 변수(2017)가 이미 대신했다 |
컨테이너 쿼리 @container | 뷰포트가 아니라 부모 크기에 반응하는 컴포넌트 단위 반응형 |
:has() | 자식 상태로 부모를 고르는, JS로 하던 선택 |
| 앵커 위치 지정 | 툴팁·드롭다운을 기준 요소 옆에 붙이던 Popper·Floating UI 같은 JS 위치 계산 |
도구 쪽도 이 흐름을 따른다. React 서버 컴포넌트와 맞지 않는 런타임 CSS-in-JS는 빌드 시점에 CSS를 뽑는 zero-runtime 방식으로 옮겨 갔고, Tailwind v4는 설정을 JS 파일이 아니라 CSS(@theme)에 쓰고 내부적으로 @layer를 쓴다. 지금의 CSS는 유틸리티 CSS, 컴포넌트 범위 CSS(CSS Modules·zero-runtime), 그리고 이를 받쳐 주는 네이티브 스펙이 함께 쓰이는 구도다.
새 기능과 도구를 따라가는 법
새 스펙을 실무에 쓸지는 브라우저 지원으로 판단한다. Baseline이 Newly available(모든 주요 브라우저의 최신 버전이 지원)이면 점진적 향상으로 쓰고, Widely available(그로부터 30개월)이면 폴백 없이 쓴다. 세부 지원은 Can I use로 확인한다(CSS 점진적 향상). 매년 열리는 State of CSS 설문은 새 스펙의 인지도·사용률과 라이브러리 만족도를 보여 준다. 라이브러리는 사용률과 만족도로 네 칸에 나눠 본다.
| 칸 | 사용률 | 만족도 | 의미 |
|---|---|---|---|
| Adopt | 높음 | 높음 | 대부분이 쓰는 검증된 기술 |
| Assess | 낮음 | 높음 | 관심이 높지만 검증이 더 필요한 기술 |
| Analyze | 높음 | 낮음 | 많이 쓰지만 갈아탈지 고민할 기술 |
| Avoid | 낮음 | 낮음 | 가급적 피할 기술 |
정리
- CSS가 어려운 이유는 문법이 아니라 문서용 설계로 앱을 만드는 데서 생긴 전역 범위·명시도 문제다.
- BEM, CSS Modules, CSS-in-JS, 유틸리티 CSS는 모두 이 두 문제를 피하는 서로 다른 길이고, 이제는
@layer·@scope가 CSS 안에서 같은 문제를 푼다. - 옛 우회법(float 레이아웃, 복잡한 선택자)과 지금의 정석을 구분해서 배운다.
- CSS는 깊은 이론보다 숙달이 중요하다. 나만의 베스트 프랙티스를 만들고, 새 스펙과 도구를 지켜보며 적당한 때 갱신한다.
관련 노트: 선택자와 우선순위, Float와 Flexbox, React 스타일링, CSS 기초, CSS 설계 방법론: BEM과 ITCSS, 유틸리티 우선 CSS, CSS-in-JS와 CSS Modules