예전에 CSS보다는 하스켈이 차라리 더 쉽다는 농담을 들은 적이 있는데, 당시에는 그냥 웃어 넘겼었습니다. 하지만 지난 몇 달 동안 CSS를 작성해보았더니 왜 그 사람이 그런 말을 했는지 이제는 이해할 수 있습니다. CSS는 복잡합니다. 문법과 구조는 간단하지만 결과물이 런타임에 따라 천변만화하기 때문에 어떤 결과물이 나올지 예측하기 정말 어렵습니다.

세 가지 문제

제가 보기엔 문제의 원인은 크게 세 가지로 나눌 수 있습니다.

  1. 어떤 CSS 규칙이 최종적으로 적용될지 예측하기 어렵다
  2. CSS 규칙의 상호작용을 모두 알기 어렵다
  3. 런타임 환경을 예상하기 어렵다

어떤 CSS 규칙이 최종적으로 적용될지 예측하기 어렵다

CSS에서는 같은 CSS규칙을 여러번 선언할 수 있습니다. 중복 선언된 규칙 중 어떤 것이 최종적으로 적용될지는 cascade라는 알고리즘에 따라 결정되는데, 간단한 알고리즘이기 때문에 어떤 규칙이 적용될지는 쉽게 예측할 수 있습니다. 문제는 선언된 모든 규칙을 빠짐없이 파악하는 부분입니다.

먼저 CSS 속성은 어디서든 선언될 수 있습니다. 이는 보통 글로벌 네임스페이스 문제라고 부르는데, 이 때문에 CSS의 최종 결과값을 알기 위해서는 해당 문서 파일과 함께 불러온 모든 CSS 파일 내의 모든 CSS 속성을 살펴봐야합니다. 수십 줄 정도를 살펴보는 것은 일도 아니지만 수십 개의 파일에 걸쳐 선언된 수천 줄의 CSS를 실수 없이 뒤져보는 것은 현실적으로 불가능한 일입니다.

또한 DOM 요소는 부모 요소로부터 CSS 속성을 상속할 수도 있습니다. 따라서 해당 요소의 특정 CSS 값을 알기 위해서는 모든 부모 요소의 CSS 값도 파악해야 합니다. 예를 들어 DOM 트리 말단에 위치한 요소의 폰트 값은 실제로는 해당 요소의 머나먼 조상 요소에 선언되었고, 해당 요소는 이를 상속한 것일 뿐일수도 있습니다.

그리고 특정 DOM 요소에 대한 CSS 속성을 선언하는 방법이 너무나도 다양합니다. 다음 HTML 코드를 예로 들어보겠습니다.

<div class="container">
  <p id="content" class="sample-text">
    <span>Sample Text</span>
  </p>
</div>

여기서 span 태그를 선택할 수 있는 방법은 수십가지 넘게 있습니다. 단일 span 타입 선택자, 타입 선택자를 중첩한 div p span, 클래스 선택자, 아이디 선택자, 타입 선택자를 조합한 .container #content span, 클래스 선택자, 타입 선택자를 조합하되 일부만 명시한 .sample-text span 등 다양합니다. 그렇기 때문에 어떤 속성이 어떤 요소에 적용될지 확실하고 손쉽게 파악할 수 없습니다.

이론적으로는 특정 DOM 요소에 적용되는 CSS 규칙을 빠짐없이 찾으려면 모든 CSS 파일의 모든 CSS 규칙을 꼼꼼하게 살펴봐야 합니다. 물론 실제로는 경험을 바탕으로 적절히 추측할 수 있기 때문에 그 정도까지 가는 일은 없습니다. 하지만 최초의 추측이 틀렸을 때는 CSS의 고질적인 문제를 마주해야 하며, 이 때 CSS를 디버깅하는 과정은 논리적인 문제 해결 과정보다는 운에 맡기고 이것저것 시도해보는 것에 가깝게 느껴집니다.

CSS 규칙의 상호작용을 모두 알기 어렵다

CSS 속성은 대부분 다른 속성과 상호작용을 하여 결과값을 내는데, 덕분에 복잡성이 더더욱 증가합니다. background-color, text-decoration, cursor 등의 속성은 다른 CSS 속성과 무관하게 결과값을 내지만 이런 속성은 몇 없습니다.

어떤 속성끼리 상호작용하는지 예측이 가능한 경우도 가끔 있습니다. font-size, font-weight, font-family가 모두 타이포그래피에 관련된 속성이라는 것은 직관적으로 알 수 있습니다. 하지만 대부분의 경우 속성의 이름만 보고 상호작용을 예상하기는 어렵습니다.

예를 들어 border 속성은 widthheight 값에 영향을 줄까요? 보통은 그렇지 않지만 box-sizing 속성값에 따라서 영향을 줄 때도 있습니다. font-size 값을 증가시킨다면? 보통은 영향을 주지만 overflow 속성값에 따라서 영향을 주지 않을 수도 있습니다. 자식 요소의 크기는 부모 요소의 width 값이 영향을 줄까요? 보통은 영향을 미치지만 자식 요소의 position 속성값에 따라 영향을 미치지 않을 수도 있습니다. 대부분의 CSS 속성이 이런 식입니다.

런타임 환경을 예상하기 어렵다

웹브라우저의 버전과 개발사, 심지어 운영체제도 CSS 결과값에 영향을 줍니다.

각 웹브라우저는 CSS 스펙을 독자적인 방식으로 구현합니다. 최신 브라우저에서는 구현 방식별 차이점 문제가 두드러지지 않으나, 마이크로소프트 사의 브라우저는 여전히 타사의 브라우저와는 조금 다르게 동작하는 경우가 있습니다. 구버전 브라우저의 경우 새로 도입된 CSS 속성을 지원하지 않으므로 주의해야 합니다.

사용자의 브라우저 설정도 고려해야 합니다. 확대를 했거나 텍스트 크기를 바꿨을 수도 있고, 사용자가 직접 정의한 CSS가 있을 수도 있습니다. 이럴 경우 결과값에 큰 영향이 있을 수 있습니다.

또한 운영체제 설정도 영향을 미칠 수 있습니다. MacOS와 윈도우즈는 폰트 렌더링 방식이 다르기 때문에 font-weight 값이 같더라도 MacOS에서는 폰트가 조금 더 두껍게 그려집니다. 운영체제 자체에도 텍스트 크기 설정이 있기 때문에 타이포그래피가 생각한 것과 다르게 나올 수 있습니다.

마지막으로 사용자 기기도 문제가 될 수 있습니다. 화면 크기에 따라 레이아웃이 크게 바뀔 수 있고, 색상 조정값에 따라서 세밀하게 지정한 색상이 어그러질 수도 있습니다.

대응방법

어떤 CSS 규칙이 최종적으로 적용될지 예측하기 어렵다

글로벌 네임스페이스, 상속, 선택자 조합을 모두 함께 언급하긴 했지만 가장 근본적인 문제는 글로벌 네임스페이스입니다. 조상 속성이 몇 개 되지 않으면 상속된 속성을 파악하는 것은 문제가 되지 않고, 선택자의 경우 일정한 방식으로 사용하기로 팀 내에 관습을 정하면 됩니다.

CSS 글로벌 네임스페이스 문제에 대해서는 수십년 동안 수많은 해결책이 제시되었는데 제가 선호하는 두 가지를 소개하겠습니다. 먼저 BEM을 사용하고, 프로젝트가 충분히 복잡해지면 CSS Modules를 도입하는 것을 추천합니다.

BEM

BEM은 CSS 특유의 글로벌 네임스페이스에서도 이름이 중복되지 않도록 CSS 아이디와 클래스를 명명하는 방법론입니다. 단순하면서도 충분히 구체적이기 때문에 배우기도 쉽게 프로젝트 특성에 따라 변형시켜 사용하기도 좋습니다.

BEM은 단순한 명명법이기 때문에 자바스크립트나 SASS 같은 의존성을 추가할 필요가 없습니다. 따라서 프로젝트가 불필요하게 복잡해지지 않도록 하여 접근성을 유지하고 싶다면 매우 적절한 시작점입니다. BEM 자체도 단순하기 때문에 프로젝트가 발전하는 과정에 따라 다른 의존성을 손쉽게 추가할 수 있습니다.

하지만 수동적으로 적용하는 관습이기 때문에 그에 따른 단점은 분명히 있습니다. 먼저 모든 팀원이 BEM이 무엇인지 이해하고, 어떻게 사용할 지를 이해하고 있어야하며, 이를 따를 의사가 있어야 합니다. 하지만 BEM을 사용하는 방식에 있어 조금씩 이견이 생기는 것은 불가피한 일입니다.

CSS Modules

CSS Modules는 각 파일에 선언된 CSS 선택자에 고유한 해시 문자열을 추가하여, 해당 선택자가 각 파일의 네임스페이스에서만 동작하도록 자동적으로 관리해줍니다.

CSS Modules를 처음 설정하는 것은 훨씬 어렵습니다. 가장 일반적인 방법은 자바스크립트를 사용해서 웹팩 안에 PostCSS Loader를 추가하고, 여기서 PostCSS의 플러그인으로 불러와서 사용하는 것입니다. 기능 하나를 사용하기 위한 것치고는 의존성이 매우 많습니다. 심지어 설정하기 복잡하기로 악명높은 웹팩도 끼어있습니다.

그럼에도 불구하고 CSS Modules는 글로벌 네임스페이스 문제를 확실하고 자동적인 방식으로 해결해주기는 합니다. 프로젝트가 충분히 복잡해지면 이런 기능이 절실해질 수도 있습니다.

CSS 규칙의 상호작용을 모두 알기 어렵다

아쉽게도 CSS 속성끼리 어떻게 상호작용하는지는 경험을 통해서만 배울 수 있는 것 같고, 이름을 바탕으로 파악할 수 있는 요령은 없는 것 같습니다. 형식 체계의 근본이 되는 공리를 찾으려는 방식보다는, 외국어를 배울 때 새로운 단어를 배우는 마음으로 여유롭게 하나씩 배우는 것이 최선의 방법이라 생각합니다.

런타임 환경을 예상하기 어렵다

이 문제는 코드를 통해서 해결할 수 없는 문제입니다. 사용자의 런타임 환경을 바꾸는 것도, 모든 런타임 환경에서 완벽하게 동작하는 CSS를 작성하는 것도 불가능합니다. 어쩔 수 없는 일 때문에 스트레스를 받으며 시간을 낭비하기보다는 현실을 직시하는 편이 낫습니다. 그보다는 어떤 런타임 환경을 대상으로 할 지 확정하고 이를 확실히 지원하는 것이 보다 현명한 접근법이라 생각합니다.

CSS를 있는 그대로 받아들이자

처음에는 CSS가 너무 불확실하고 무질서하게 느껴져서 매우 싫었습니다. 하지만 무엇인가 새로운 것을 배울 때는 마음을 열고 받아들여야한다는 말을 되새기며 CSS를 기존 관점에서 평가하기보다는 CSS의 관점에서 살펴보려고 노력했습니다. CSS를 있는 그대로 받아들이기로 하고보니 충분히 유용한 도구라는 점도 인정하게 되었습니다. 가끔 기묘하게 동작해서 할 말을 잃게 만드는 일은 여전히 있지만 작성하면서 조금씩 재미도 느끼게 되었습니다. 여전히 마음에 쏙 드는 언어는 아니지만 그냥저냥 쓸 수는 있을 것 같습니다.