CSS Variable을 이용해 손쉽게 다크/라이트 토글 가능한 스타일시트 만들기

2020/05/29 00:11

이번에 블로그를 워드프레스로 마이그레이션하면서, 디자인을 완전히 새로 갈아 엎은 건 아니지만 그래도 오랜만에 작업하는김에 이곳 저곳 손 본 곳이 꽤 된다. 그중에 가장 눈에 띄는 변화라고 한다면, 아마 라이트모드의 부활일 것이다.

현재 굴리고 있는 블로그의 디자인은 2017년에 처음 런칭한 것으로, 처음에는 밝은 배경의 테마였다가 2018년에 페이스리프트를 거치면서 다크모드화 되었다. 이미지 체인지를 꾀한것도 있지만, 그 시기가 한창 다크모드가 아이폰, 맥 등에 도입되기 시작하면서 ‘핫’할 시기였기 때문에, 평소 폰에서 다크모드를 사용하는지라 그에 걸맞게 블로그의 배경도 어둡게 만들고 싶었던 것이 동기부여가 되었었다.

그러다 최근에는, 다크모드 고정이 아닌 시간에 따라 라이트/다크 자동으로 전환되는 옵션을 이용하고 있다. 낮에 주변 조광 상태가 충분한 환경에서는, 흰 배경에 검은 글씨를 읽는게 좀 더 자연스러운 것 같기도 하고, 단순히 다크모드 UI가 질려서(?) 밝은게 더 이뻐보인다는 생각이 들기도 하고.

아무튼 그래서 내가 하고 싶은 말은, 앱이든 웹사이트든 어느 한쪽만 제공하는게 아니라, OS의 설정에 맞춰 그에 알맞게 라이트/다크 양쪽에 최적화된 테마를 다 제공하면 가장 유저 프렌들리하지 않을까 하는 이야기.

이 글에서는 한 색상에 고정되어있던 CSS를 상황에 따라 두 테마간 전환이 가능한 구조로 만드는 전략, 방법에 대해 설명해보고자 한다.


유저의 선호 테마 설정 읽기

prefers-color-scheme이라는 미디어 쿼리가 있다. “모바일 반응형 웹”을 만들때 흔히 활용하는 미디어 쿼리인 min-width,max-width같이 브라우저에서 주는 정보중 하나인데 여기에서 유저가 사용중인 환경의 컬러 테마가 Light인지 Dark인지에 대한 힌트를 얻을 수 있다.

prefers-color-scheme은 아직 CSS3 Media Queries에 정식으로 포함된 스펙은 아니고, 이 다음 단계인 Level 4도 아니고 무려 그 다음인 Level 5의 작업중인 스펙으로 등재되어있는 녀석이다. 그런 것 치고는, 2020년 5월 현재 기준 이미 대다수의 메이저 브라우저들에서 문제없이 지원되고 있다.

Data on support for the prefers-color-scheme feature across the major browsers from caniuse.com

이걸 통해 우리는 CSS상 라이트모드와 다크모드의 스타일을 구분할 수 있다:

/* 라이트 */
@media (prefers-color-scheme: light) {
  .foo {
    color: black;
  }
}

/* 다크 */
@media (prefers-color-scheme: dark) {
  .foo {
    color: white;
  }
}

그러면, 대충 이제 어떻게 해야할지 어렴풋이 감이 온다. 기존에 라이트로 디자인되어있던 스타일시트 위에, 색을 덮어씌우고싶은 항목들만 골라내 @media (prefers-color-scheme: dark)안에 오버라이드를 쓰면 될 것이다. 간단…하다?

그런데 정말 그냥 그렇게 했다가는, 유지보수가 지옥이 될 것 같다. 앞으로는 한 부분의 디자인을 건드리려 하면 메인 스타일시트를 바꾸고, 코드 저어어기 아래나 별도 파일에 있는 다크모드 오버라이드도 잊지 말고 바꿔야하기 때문이다. 이것보다는 좀 더 똑똑하게 관리할 방법이 있을 것 같다.

variable로 어떻게 못 해요?

2020년에 CSS를 하고 있다면 아마 높은 확률로 Preprocessor (전처리)를 사용하고 있을거라 생각한다. (안 한다구요? 인생 손해 보고 계십니다. 당장 검색해 배우고 오세요) 나는 개인적인 선호 (와 익숙함)에 의해 LESS를 쭉 쓰고 있는데, 대세라고 하면 역시 Sass일듯 하다. 아무튼 어느쪽을 쓰든 상관 없다. 서로 다른 점도 많이 있지만 CSS 전처리기는 대부분 자체적인 방식으로 ‘변수'(variable)을 쓸 수 있도록 구현해 놓았다. 즉, CSS안에서 반복 사용되는 속성값들 (예: 색상값, 너비 수치 등)을 한번 선언해주고 이후로는 값을 직접 입력하는게 아니고 변수를 써주면 나중에 혹여나 색을 바꿔야할때 전부 검색해서 일일히 바꿔줘야하는 귀찮음을 피할 수 있는 거다.

// 소스 (LESS)
@my-color: #fcba03;

.header {
  color: @my-color;
}
.button {
  border: 2px solid @my-color;
}

// 실제 아웃풋
.header {
  color: #fcba03;
}
.button {
  border: 2px solid #fcba03;
}

이미 이런 식으로 코드가 다 짜여있다면, 한 숨 덜었다. 모든 색상에 대해 전부 손으로 미디어 쿼리 오버라이드를 쓸 필요는 없으니까. 색상이 이미 variable화 되어있다면, 그 값만 라이트, 다크에 따라 다르게 설정하고 빌드할때 각각의 값을 읽어들인 두 개의 버전으로 컴파일되게 하면 된다. 이걸 하는것도 여러 방법이 있겠지만… 나같으면 style-light.less와 style-dark.less 껍데기 파일을 만들고, 실제 스타일을 지정한 코드는 main.less같은 서브파일로 몰아버리고, style-light에는 _variables-light.less를 임포트, style-dark에는 _variables-dark.less를 임포트해두고 두개 파일을 빌드하게 할 것 같다.

// _variables-light.less
@text-color: #333;
@bg-color: #fff;

// _variables-dark.less 
@text-color: #eee;
@bg-color: #333;

// _main.less
body {
  color: @text-color;
  background: @bg-color;
}

// style-light.less
@import '_variables-light.less'
@import '_main.less'

// style-dark.less
@import '_variables-dark.less'
@import '_main.less'


이런 식으로. 빌드가 style-light.css, style-dark.css로 나온다면, HTML에서 각각의 환경에 맞게 알맞은 스타일시트를 로드하게 하면 된다.

<link rel="stylesheet" href="/dark.css" media="(prefers-color-scheme: dark)">
<link rel="stylesheet" href="/default.css">

그런데 이렇게 되면, 빌드는 쉽지만 스타일시트가 각각의 테마에 맞게 두개가 된 셈이라, 유저 입장에서는 양쪽 테마를 번갈아가며 다 보게 된다면 실제 차이가 있는 색상 관련 외에는 사실상 똑같은 중복 코드를 또 불러들이게 된다는 말이 된다. 뭐 CSS코드 2배 되어봐야 얼마나 타격이 가겠어 싶을수 있겠지만… 1초라도 로딩 속도를 빠르게 해 고객에게 좋은 인상을 남겨야하는 프로덕트의 웹페이지라면 줄여야만 하는 소중한 킬로바이트일 수 있다. 그런거 아니라고? 그래도… 그치만… 중복 코드라니… 그냥 기분이 좀 그렇잖아…

CSS variable로 어떻게 좀 안 돼요?

오늘의 주인공 되시겠다. 앞서 소개했듯이 이미 대다수의 CSS 장인들이 전처리기를 이용해 변수의 편리함을 맛보고 있었지만, 이는 아직 네이티브 CSS, 즉 브라우저에서 직접 해석할 수 있는 코드가 아니라 반드시 CSS로 변환하는 컴파일(빌드) 과정을 거쳐야만 했다. 그러니까 처리기지…

하지만 때는 바야흐로 2020년… 이제는 CSS 네이티브 구현의 변수 시스템인 CSS variable을 프로덕션에서도 어느정도 쓸 수 있는 시대가 왔다.

Data on support for the css-variables feature across the major browsers from caniuse.com

IE가 안 된다구요? 당연히 안 되죠. 2020년에 IE를 왜 지원해야하죠? 우리회사는 지원해야된다구요? 그것 참… 진심으로 애도의 말씀을 드립니다. 정말 진짜 힘드시겠어요. 하루빨리 더 좋은 곳으로 이직하실수 있기를 바랍니다.

보시다시피 메이저 브라우저에서는 이미 과거 5개 전 버전까지도 풀 지원을 하고 있는 실정이다. 정말 레거시에 민감한 제품이 아니라면, 마음 놓고 써도 된다. (정 불안하다면, polyfill도 있겠지만… 이건 직접 찾아보세요)

기존의 전처리기에서 쓰던 변수에 비해 CSS variable을 쓰면 뭐가 좋냐면, 우선 빌드를 다 마친 후에도 클라이언트 브라우저측에서 변수의 값을 바꾸는것 만으로 실제 표시되는 화면 요소의 색도 바꿀 수 있다는 점이다.

가령 아까 위에 예제로 썼던 코드를 CSS 변수로 바꿔쓴다면 이렇게 된다:

:root {
  --text-color: #333;
  --bg-color: #fff;
}

body {
  color: var(--text-color);
  background: var(--bg-color);
}

CSS 변수는 기존 속성이름과 구분을 해야 하다보니 --라는 prefix를 이용하고, 실제 속성에서 값으로 불러들일때는 var()안에 변수명을 넣는 식으로 쓰는 문법을 따른다. Sass나 Less같은 기존 전처리기에 익숙해진 사람이면 몇번이고 봐도 좀 낯설어보이는 신택스이긴 하다.

아무튼 네이티브 CSS 코드에서는 빌드 단계가 없으니 중간에 ‘버려지는’ 코드가 없고 전부 다 저장한 그대로 출력되다보니, 변수들이 그냥 문서 아무데나 있을수 있는건 아니고 CSS의 기본 구조에 따라 어떠한 선택자(selector) 괄호 {}안에는 들어있어야 한다. 아무데나 넣어도 되지만 보통 색상 등 전역 변수로 사용한다면 아무튼 최상단에 올려야할 것인데, :root는 HTML DOM 구조상 최상위 엘리먼트인 <html>보다도 우선권이 높다. 그래서 네이티브 CSS에서 보통 변수를 선언하면 문서 최상단에 저렇게 :root안에 넣어 먼저 선언을 해버리는것.

자, 그러면 여기다가 다크모드에서 다르게 보이는 색을 지정해주고 싶으면?

:root {
  --text-color: #333;
  --bg-color: #fff;
}

@media (prefers-color-scheme: dark) {
  :root {
    --text-color: #eee;
    --bg-color: #333;
  }
}

body {
  color: var(--text-color);
  background: var(--bg-color);
}

이런 식이 된다. 이러면 단일 스타일시트에서, 메인 레이아웃, 디자인의 코드는 단일 코드로 유지하고 색상 팔레트 등만 두개 버전이 들어간 중복된 코드 없는 이상적인 스타일시트가 된다.

맨바닥부터 애초에 CSS 변수를 사용해 스타일시트를 짠다고 하면 차라리 쉬울지 모르겠다. 문제는 기존에 이미 전처리기 변수에 의존해 짜둔 구조의 스타일시트를 이 형태로 어떻게 변환할까이다. 기존 스타일 구조와 환경에 따라 생각보다 까다로울 수도 있다.

(다음 글에서 계속)