"CSS는 프로그래밍이 아니잖아요."
신입 시절에 회식 자리에서 누군가 이런 말을 했다. 백엔드 개발자였다. 웃으면서 한 말이었고, 악의는 없었을 거다. 나도 그때는 대충 웃어넘겼다. 왜냐하면 그때는 나도 그렇게 생각했으니까.
2년이 지난 지금은 생각이 완전히 다르다. CSS는 어렵다. 진짜 어렵다. 어려운 정도가 아니라 예측 불가능하다. 그리고 예측 불가능하다는 건, 제대로 이해하고 있지 않다는 뜻이다.
4시간짜리 specificity 버그
지난해에 있었던 일이다. 디자인 시스템에서 버튼 컴포넌트의 스타일을 수정하는, 아주 사소한 작업이었다. secondary 버튼의 호버 색상을 바꾸는 거. 5분이면 될 줄 알았다.
.btn-secondary:hover의 background-color를 바꿨다. 로컬에서 확인했다. 적용이 안 됐다. 왜?
DevTools를 열었다. 내 스타일이 취소선이 그어져 있었다. 다른 규칙에 의해 덮어씌워지고 있었다. 범인을 찾아보니 .card .btn.btn-secondary라는 셀렉터가 있었다. 누가 언제 이걸 작성한 건지 git blame을 돌려봤다. 8개월 전. 이미 퇴사한 사람이 작성한 코드였다.
이 셀렉터의 specificity가 더 높았다. 클래스 3개 vs 클래스 1개 + 가상 클래스 1개. 당연히 전자가 이긴다.
"그러면 specificity를 높이면 되지." 그래서 .card .btn.btn-secondary:hover로 바꿨다. 됐다. 끝. 이라고 생각했는데 다른 페이지에서 같은 버튼이 또 다르게 보였다. 거기는 .modal .content .btn-secondary라는 셀렉터가 있었다. 또 specificity 싸움.
이걸 하나하나 잡기 시작하면 끝이 없다. specificity를 높이면 다른 곳에서 또 깨진다. 이건 두더지 게임이다. 머리를 하나 때리면 다른 데서 올라온다.
결국 근본 원인은 디자인 시스템의 CSS 구조 자체에 있었다. 컴포넌트 스타일이 전역 CSS로 관리되고 있었고, 여기저기서 specificity를 높여가면서 덮어쓰는 패턴이 쌓여 있었다. 8개월 전 그 코드는 당시에는 문제를 해결했겠지만, 시한폭탄이었던 거다.
버튼 호버 색상 하나 바꾸는 데 4시간이 걸렸다. 그리고 이건 근본적으로 해결한 게 아니라 일단 그 페이지들에서만 안 깨지게 패치한 거다. 진짜 해결은 CSS 구조를 리팩토링하는 것이었는데, 그건 몇 주짜리 작업이었다.
Safari가 등장하면 모든 게 변한다
크로스 브라우저 호환성이라는 게 있다. 이론적으로는 CSS 명세가 있고, 브라우저가 그 명세를 구현하니까 어디서든 같게 보여야 한다. 현실은 다르다. 특히 Safari.
한번은 flexbox 레이아웃이 Safari에서만 깨졌다. Chrome, Firefox에서는 완벽했다. Safari에서만 아이템의 높이가 이상하게 늘어났다. 원인은 flex 아이템 안에 이미지가 있었는데, Safari가 이미지의 기본 aspect-ratio를 다르게 처리하는 거였다.
또 한번은 gap 속성이 Safari 구 버전에서 먹히지 않았다. flexbox의 gap은 비교적 최근에 Safari가 지원하기 시작했다. 근데 우리 사용자 중 상당수가 아이폰을 쓰고, iOS의 Safari 업데이트는 OS 업데이트에 묶여 있어서, 꽤 오래된 버전의 Safari가 살아 있었다.
position: sticky도 Safari에서 독특하게 동작한다. overflow 속성이 있는 조상 요소의 범위 안에서만 스티키가 작동하는 건 모든 브라우저가 같지만, Safari는 그 범위를 해석하는 방식이 미묘하게 다를 때가 있다. 이걸 디버깅하면서 반나절을 날렸다.
이런 이슈들을 겪으면 "Can I Use"를 습관적으로 확인하게 된다. 그리고 모든 CSS 속성에 대해 "Safari에서도 되나?"를 자동으로 의심하게 된다. 이 습관이 형성되기까지 꽤 많은 핫픽스를 배포했다.
"그냥 Tailwind 쓰면 되잖아"
이런 말을 종종 듣는다. Tailwind를 쓰면 CSS의 복잡한 부분을 몰라도 된다는 뉘앙스. 나도 Tailwind를 쓴다. 생산성이 좋다. 클래스 이름 고민 안 해도 되니까 빠르다.
근데 Tailwind가 CSS를 대체하는 건 아니다. Tailwind는 CSS 위에 있는 추상화다. CSS를 모르고 Tailwind를 쓰면, Tailwind의 유틸리티 클래스가 뭘 하는 건지 모르면서 쓰는 거다. 작동하는 것처럼 보이지만, 뭔가 이상하게 동작할 때 디버깅을 못 한다.
flex items-center justify-between을 붙이면 원하는 레이아웃이 나온다. 그런데 특정 화면 크기에서 아이템이 넘쳐서 잘린다면? flex-wrap을 추가해야 하는데, 왜 flex-wrap이 필요한지 이해하려면 flexbox의 기본 동작을 알아야 한다. flex 아이템은 기본적으로 줄바꿈 없이 한 줄에 다 들어가려 하고, min-width: auto가 기본값이라 내용물보다 작아지지 않는다는 것. 이걸 모르면 Tailwind를 써도 레이아웃이 깨질 때 멘붕이 온다.
Ahmad Shadeed라는 CSS 전문 개발자가 "Defensive CSS"라는 개념을 정리했다. 미래에 깨지지 않을 CSS를 미리 방어적으로 작성하는 방법론인데, 이 사람의 글을 읽다 보면 CSS가 얼마나 예측 불가능한 상황을 만드는지 잘 보인다.
예를 들어 이런 것들이다. height: 350px을 hero 섹션에 주면, 내용이 많아졌을 때 overflow가 발생한다. min-height: 350px을 써야 한다. justify-content: space-between으로 아이템을 배치하면, 아이템이 예상보다 적을 때 양 끝에 붙어서 어색해진다. gap을 쓰는 게 더 안전하다. 스크롤바가 나타나면서 레이아웃이 밀리는 현상은 scrollbar-gutter: stable로 방어할 수 있다.
이런 디테일 하나하나가 실전에서 버그로 나타난다. 그리고 이건 Tailwind를 쓰든 CSS Modules를 쓰든 styled-components를 쓰든 똑같이 알아야 하는 것들이다. 도구가 뭐든 결국 CSS 자체의 동작을 이해해야 한다.
Stephanie Eckles가 운영하는 ModernCSS.dev에도 비슷한 맥락의 컨텐츠가 많다. 셀렉터 마스터리, 폼 요소 커스텀 스타일링, 미디어 쿼리 없이 반응형 만드는 방법 같은 것들. 하나하나가 "이거 알아야 실전에서 안 깨진다"는 내용이다. CSS의 기본기가 탄탄하지 않으면 이 중 아무것도 제대로 이해할 수 없다.
CSS의 반직관적인 부분들
CSS가 어려운 이유 중 하나는 직관적이지 않다는 거다. 경험과 다르게 동작하는 경우가 많다.
margin 겹침(collapse) 현상이 대표적이다. 위아래로 인접한 두 요소에 각각 margin-bottom: 20px, margin-top: 30px을 주면 둘 사이 간격이 50px일 것 같지만 30px이다. 큰 쪽이 이긴다. 이건 규칙이 있긴 한데, 그 규칙이 여러 조건에 따라 달라진다. 부모 요소에 padding이나 border가 있으면 겹침이 안 일어난다든가, flexbox나 grid 컨텍스트 안에서는 안 일어난다든가.
z-index도 단순해 보이지만 실전에서는 지옥이다. z-index: 9999를 줬는데 밑에 깔리는 상황. stacking context라는 개념을 이해해야 한다. position, opacity, transform, filter 같은 속성이 새로운 stacking context를 만든다. 부모가 z-index: 1이면 자식에 z-index: 99999를 줘도 부모 밖으로 못 나간다.
width: 100%가 의도와 다르게 동작하는 경우도 잦다. 부모의 padding을 포함하는 건지 아닌지는 box-sizing에 달려있다. border-box를 전역으로 설정 안 해놓으면 계산이 미묘하게 어긋나면서 가로 스크롤이 생긴다. 대부분의 리셋 CSS가 box-sizing: border-box를 전역으로 설정하는 이유가 이거다.
이런 것들을 하나하나 익히는 데 시간이 걸린다. 그리고 안 겪어보면 모른다. 문서를 읽어도 실감이 안 난다. 직접 레이아웃이 깨지고, 삽질하고, 원인을 찾고 나서야 "아 이게 이렇게 동작하는 거구나" 하게 된다.
그럼에도 CSS를 제대로 배워야 하는 이유
여기까지 읽으면 "CSS 힘들다 그만하고 싶다"가 될 수 있는데, 반대다. 이래서 제대로 배워야 한다는 거다.
CSS를 잘하는 프론트엔드 개발자와 못하는 프론트엔드 개발자의 차이는 시간이 지날수록 벌어진다. JavaScript는 에러가 나면 콘솔에 빨간 글씨가 뜬다. 뭐가 잘못됐는지 알려준다. CSS는 조용히 잘못된다. 에러 메시지가 없다. 그냥 화면이 이상하게 보인다. 그리고 왜 이상한지 추론해야 한다. 이 추론 능력이 CSS 실력이다.
디자이너가 시안을 주면 "이건 CSS로 어려운데요"라고 말하는 게 아니라, "이건 이렇게 구현하면 되는데, 이 부분은 Safari에서 이슈가 있을 수 있으니 대안을 같이 정하면 좋겠어요"라고 말할 수 있어야 한다. 이게 전문성이다.
CSS를 잘하면 JavaScript에 대한 의존도도 줄어든다. 많은 인터랙션이 CSS만으로 가능하다. 트랜지션, 애니메이션, 반응형 레이아웃, 다크모드, 스크롤 기반 효과. JavaScript로 억지로 구현하면 성능도 떨어지고 코드도 복잡해진다. CSS가 네이티브로 할 수 있는 걸 CSS로 하면 성능도 좋고 코드도 깔끔하다.
CSS는 프로그래밍이다
CSS에는 변수(custom properties)가 있다. 계산(calc, min, max, clamp)이 있다. 조건 분기(@media, @supports, @container)가 있다. 스코프가 있다(cascade layers, @scope). 상속이 있다. 요즘은 @property로 타입까지 지정할 수 있다.
이게 프로그래밍이 아니면 뭘까. 문법이 중괄호와 세미콜론이라서 프로그래밍 같아 보이지 않을 뿐, 로직을 작성하고, 조건에 따라 다르게 동작하게 하고, 변수를 관리하고, 스코프를 신경 쓰는 건 다른 언어와 다르지 않다.
다만 CSS의 독특한 점은 선언적이라는 거다. "이렇게 해라"가 아니라 "이 조건에서는 이런 상태이다"를 기술한다. 그래서 명령형 프로그래밍에 익숙한 사람에게는 사고 방식 자체가 전환이 필요하다. 이 전환이 쉽지 않기 때문에, CSS가 어렵게 느껴지는 거다.
CSS가 쉽다고 말하는 사람은 두 종류다. CSS를 정말 잘하는 사람이거나, CSS로 뭔가 복잡한 걸 만들어본 적이 없는 사람이거나. 후자가 훨씬 많다.
만약 "CSS 좀 할 줄 안다"고 생각하면, Ahmad Shadeed의 Defensive CSS 글을 한번 읽어보길 권한다. 자기가 모르는 게 얼마나 많은지 알게 된다. 나도 그랬다.
CSS는 겸손하게 대해야 하는 기술이다. 쉬워 보이는 순간, 반드시 뒤통수를 친다.
