효율적인 다크 모드 구현을 위한 배경/전경 컨텍스트 기반의 컬러 팔레트 만들기 (feat. CSS variable)

2021/09/23 01:56

지난 글에서 스타일시트 메인 코드상의 색상 지정은 CSS variable (변수)로, prefers-color-scheme미디어 쿼리를 이용해 유저의 선호 테마를 알아내어 상황에 알맞는 값을 줘서 단일 코드로 여러 테마를 표시할 수 있는 방법에 대해 알아보았다.

/* 변수 선언 */
:root {
  --text-color: black;
  --bg-color: white;
}

@media (prefers-color-scheme: dark) {
  :root {
    --text-color: white;
    --bg-color: black;
  }
}

/* 스타일 선언 */
body {
  color: var(--text-color);
  background: var(--bg-color);
}

간략하게 복습해보자면, 이런 식으로 실제 스타일 선언은 한 번만 하고, 값에 CSS 변수를 넣어 테마에 따라 입력 값이 바뀌게 하는 식이다.

만약 위와 같이 지정할 색이 달랑 배경색과 텍스트색만 있었다면 사실 별 고민도 안하고 그냥 아무렇게나 했어도 됐겠지만…..

현실은 그리 녹록치 않다…

왼쪽은 다크모드를 적용한다면 어떻게 할지 알겠는데 오른쪽같이 복잡하게 여러가지가 들어간 색은 어떻게 바꿔야 할지 벌써부터 머리가 아프다. 하나씩 다 손으로 바꿔야만 하는 노가다만큼은 제발 피하고 싶은데, 뭔가 규칙같은게 없을까?


다크 모드를 위한 “색 뒤집기” (feat. HSL)

일단, 간단한 것부터 시작해보자.

“흰 배경 위에 검은 텍스트 -> 어두운 배경 위에 밝은 텍스트” 니까, 우선 배경색과 텍스트 색을 뒤집어 본다.

배경색, 텍스트 색만 반전시킨 예시

그래… 배경이 어두워졌으니까 다크 모드는 다크 모드지 뭐. 🤣 그렇지만 역시 우려했던 대로, 뭐 하나라도 배경색이 살짝 깔려있거나, 중간 밝기의 색들이 지정된 부분들이 문제다. 본래 디자인에서는 본문과 영역을 구분하기 위해 옅은 그레이 배경을 깔아놓았던 박스의 배경색이 바뀌지 않은 채 글자색만 반전되어버려서, 읽을 수가 없게 되어 버렸다.

라이트 모드에서는, 배경이 밝다는 전제 하에 본문 텍스트(검은색) -> 하위 단계 텍스트 (짙은 회색) -> 그 아래 덜 중요한 텍스트 (옅은 회색) 같은 식으로 시각적 위계를 구분해 왔었다. 그런데 이제는 배경이 어두워졌으니, 그 순서도 다 뒤집어져야 맞다.

이 규칙에 따라서, 컨텍스트 의미적으로 모든 색을 다크 모드에 맞게 반전시켜 준다면, 아래와 같이 된다:

모든 색을 대칭 반전시킨 예시

위의 예시를 코드로 구현해보자. 스타일 선언은 한 번만 하고, CSS 변수로 라이트/다크 모드의 색을 정의한다고 하면 이렇게 적을 수 있다:

:root {
  --page-bg-color: hsl(0, 0%, 100%);
  --header-text-color: hsl(0, 0%, 20%);
  --subheader-color: hsl(0, 0%, 40%);
  --paragraph-color: hsl(0, 0%, 20%);
  --callout-bg-color: hsl(0, 0%, 90%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --page-bg-color: hsl(0, 0%, 0%);
    --header-text-color: hsl(0, 0%, 80%);
    --subheader-color: hsl(0, 0%, 60%);
    --paragraph-color: hsl(0, 0%, 80%);
    --callout-bg-color: hsl(0, 0%, 10%);
  }
}

참고로 위의 예제는 색상을 HSL 포맷으로 적고 있다. 최근 입문이 아닌 웹 디자인/개발 경력이 다소 긴 사람이라면 아마 HEX코드 (#000000)나 rgb(0,0,0) 의 형태가 더 익숙할 수도 있을 것인데, 라이트/다크 테마 양쪽에 맞춘 2가지 색의 버전이 존재해야하는 상황에서 HSL 포맷은 확실한 장점이 있다. HSL은 Hue(색), Saturation(채도), Lightness(밝기) 세가지 요소로 색을 표현하는 방식으로, 웹에서는 저렇게 hsl(0, 0%, 0%) 형태로 적는다. 괄호 안 마지막 값인 밝기가, 0~100%의 수치로 표시되다 보니 해당 색상의 밝기를 가늠하기 쉽다는 점이 좋다.

HSL을 쓰면 다크 모드를 위해 어떤 색의 밝기 기준으로 반대에 있는 색을 계산하기도 쉽다. 단순히 생각하면 값을 50%을 기준으로 대칭해 뒤집으면 된다. 이를테면 100%의 반대는 0%, 40%의 반대는 60%.

하지만, 실전 디자인에서는 항상 모든 색이 수치상 0%~100% 밝기의 범위를 꽉 채워 사용하는 건 아니다. 보다 부드럽고 온화한 느낌을 주기 위해 다이나믹 레인지가 다소 좁게 색을 사용했을 수도 있다. 21%~95%의 범위에서 중간지점은 37%이니 이를 대칭점으로 밝기 35%의 색을 뒤집으면…??? 🤯 벌써부터 머리가 아프다. 분명 뭔가 더 좋은 방법이 있을 것만 같다.

또한, 이전 글에서 다단계 shade 팔레트를 기반으로 디자인하면 한정된 색조의 갯수를 사용함으로 일관성도 유지하고 관리도 쉬워진다는 점을 이미 배웠는데, 위의 예시는 반복되는 동일한 값의 색조차 반복해서 수동으로 선언하고 있다. 팔레트의 색을 CSS변수화해서 연결시킬 방법은 없을까?


색조 팔레트를 활용해 보자

이전 글에서 이야기한 팔레트 이야기를 요약해보자면:

  • 대부분 디자인 프로젝트의 스코프 안에서 실질적으로 사용되는 색의 갯수는 한정되어 있다
  • 각 색깔(hue)별 필요한 만큼 세분화한 Shade/Tone(색조)의 스텝으로 간격을 나누고 이름을 붙인 팔레트 시스템을 구축해두면, 디자인 (특히 UI)에 있어 반복되는 색의 사용이 편리해진다

로 정리해볼 수 있다.

그리하여 우선 ColorBox로 블루톤이 살짝 들어간 그레이의 shade 팔레트를 만들어보았다. 각 색 간의 간격은 linear로 설정하고, 총 11단계 + 양 끝에 Minor step을 추가해 총 13단계가 된 팔레트이다 (프리셋 링크). 이 팔레트는 가장 표준적인 풀 레인지의 밝기를 표현하기 위해 양쪽 끝 색상에 100% 흰색 (#fff)와 100% 검은색 (#000)을 포함시키고 싶은데, 현재 ColorBox 구현의 특성상 채도를 넣고서 맨 처음 색상과 끝색상이 완전히 흰색/검은색으로 고정되게하기 어렵기 때문에, 나중에 끝에 수동으로 #fff#000을 넣도록 한다. 이러면 총 15단계가 된다.

여기서 가져온 색상들로, 000100까지의 넘버링을 사용해 구분하는 시스템으로 이름을 붙여보면, 아래처럼 정리가 된다.

여기까지 왔으면, 이제 위의 색들로 CSS 변수 목록을 만들 수 있다. ColorBox에서 뽑은 색상들의 Hex컬러를 복사해 HSL 포맷으로 변환하고 hsl() 껍데기 부분을 떼어낸 내부 값만 남겼다. (이렇게 한 이유는 글의 뒷부분에서 알게 될 것이다)

// LESS mixin
// HSL raw values
#colors() {
  gray-000: 228, 100%, 100%; // #ffffff
  gray-005: 228, 100%, 98%;  // #f5f7ff
  gray-010: 226, 35%, 93%;   // #e6e9f3
  gray-015: 228, 24%, 88%;   // #d8dbe7
  gray-020: 227, 17%, 77%;   // #bcc0cf
  gray-030: 229, 13%, 67%;   // #a1a5b6
  gray-040: 227, 11%, 57%;   // #878c9e
  gray-050: 227, 9%, 48%;    // #6f7486
  gray-060: 226, 11%, 39%;   // #585d6e
  gray-070: 224, 13%, 30%;   // #424755
  gray-080: 228, 14%, 21%;   // #2e313d
  gray-085: 228, 16%, 13%;   // #1b1d25
  gray-090: 231, 16%, 8%;    // #121319
  gray-095: 225, 18%, 4%;    // #090a0d
  gray-100: 225, 0%, 0%;     // #000000
}

// usage example
// color: hsl(#colors[gray-050])

위의 예시는 Less의 Mixin을 활용한 변수 map형식으로 적었는데, Less/Sass같은 전처리기를 안 쓰고 플레인 CSS로 한다면 그냥 CSS variable로 --gray-000으로 해도 된다. 내 경우에는 어차피 전처리기를 쓰고 있기 때문에, 그리고 이 색상값들은 앞으로 직접 변경할 일이 없는 상수에 가까운 변수라 컴파일시에만 참조하고 이후에는 브라우저가 굳이 불필요하게 매번 찾아봐도 되지 않게 하기 위해 이렇게 했다. (CSS 변수가 많을수록 페이지 스타일을 읽어 렌더해야하는 클라이언트 단의 부하가 약간이나마 늘어난다는 건 사실인 듯 하다)

이제 색상별 변수가 생겼으니, 페이지의 각 부분에 색을 선언해야할때 그냥 색 이름과 숫자를 입력해 불러내면 된다!

:root {
  --page-bg-color: hsl(#colors[gray-000]);
  --header-text-color: hsl(#colors[gray-090]);
  --subheader-color: hsl(#colors[gray-040]);
  --paragraph-color: hsl(#colors[gray-090]);
  --callout-bg-color: hsl(#colors[gray-015]);
}

@media (prefers-color-scheme: dark) {
  :root {
    --page-bg-color: hsl(#colors[gray-100]);
    --header-text-color: hsl(#colors[gray-010]);
    --subheader-color: hsl(#colors[gray-060]);
    --paragraph-color: hsl(#colors[gray-010]);
    --callout-bg-color: hsl(#colors[gray-085]);
  }
}

이렇게 하면 이제 색의 수치상으로 계산해 반대되는 색을 찾아야하는 수고는 덜었다. 변수명의 숫자가 10의 단위로 딱 떨어지니 (5, 15, 85, 95 제외), gray-090에 상응하는 반대 밝기의 색이 gray-010라는건 별로 머리를 굴리지 않아도 금세 알 수 있다.

하지만… 여기까지 한다 해도 여전히, “UI 요소가 늘어날 때마다 그 갯수만큼이나 색 변수가 2개씩 늘어나는 문제”는 아직 해결되지 않았다.

라이트모드나 다크모드나 색깔이 뒤집어질 뿐이지 결국 전체적으로 사용되는 색은 팔레트의 어떤 색일 터인데… 이를 어떻게 잘 체계화해서 내 수고를 덜어줄 똑똑한 방법은 없는걸까?

컨텍스트 기반의 배경/전경 색상 변수 시스템

가만 생각해보니, UI에서 사용되는 모든 색은 어떤 절대적인 색상(Hue)이나 밝기를 가졌냐와 상관없이, 그 색이 사용된 컨텍스트가 어떠한가에 따라 그 역할이 크게 두 가지로 나뉘는 것을 깨달았다. 배경(background)으로 쓰였는가, 전경(foreground)으로 쓰였는가.

흰색과 검은 색을 생각해보자. 두 색은 서로 대비되는 색이지만, 어느 컨텍스트에서 사용되었는가에 따라 그 역할이 다르다.

  • “흰색”은 라이트 테마에서는 가장 옅은 색(배경)이고, 다크 테마에서는 가장 짙은 색(전경)이다.
  • “검은색”은 라이트 테마에서는 가장 짙은 색(전경)이고, 다크 테마에서는 가장 옅은 색(배경)이다.
  • 배경에 대비해 볼 때 가장 옅은 색은, 배경 색으로 쓰인다.
  • 배경에 대비해 볼 때 가장 짙은 색은, 전경 색으로 쓰인다.

위의 그림에서 Gray-090을 주목해보자면, 라이트 모드에서는 흰 배경 위에 살짝 뜬 UI 요소를 표현하는데 쓸만한 색이지만, 다크 모드에서는 반대로 메인 텍스트보다 중요도가 낮은, 살짝 옅은 텍스트의 색으로 쓰일 수 있다.

이 원리를 따라서 실제 디자인에서 색 지정을 해보자. 페이지의 배경을 ‘흰색’이 아닌 ‘가장 짙은 배경색’으로, 서브헤더의 텍스트 색을 ‘회색’이 아닌 ‘50% 농도의 전경색’으로 지정하는 거다. 배경색과 전경색이 각각 특정 테마에서 어떤 색인지, 그리고 배경색과 전경색의 옅은 색조(shade)들도 각각 실제 상응하는 색들에 연결해주면 된다.

이렇게 하면, 소스 스타일을 건드리지 않고 얼마든지 테마를 자유롭게 변경할 수 있는 플렉서블한 시스템이 완성된다.

배경/전경 색의 shade 팔레트 생성은 그리 어렵지 않다. 어떠한 전경 색의 가장 진한 버전을 foreground-100라고 이름 붙이고, 그 색이 (배경 색에 빗대어 볼 때 대비가) 약해질 수록 숫자가 0으로 수렴한다고 하자. 이렇게 색의 ‘역할’에 따라 앞서 만들었던 그레이 shade들에 숫자를 붙여보자면 아래와 같이 정리할 수 있다.

“UI 컬러 팔레트”

위와 같이 기존 색 팔레트의 위에 전경/배경의 사용 컨텍스트에 따라 새롭게 규정한 팔레트를 나는 UI 컬러 팔레트라고 부른다. UI는 브랜드 컬러를 막론하고 대체로 채도가 강하지 않은 그레이 계열 색조의 조합으로 이루어진 경우가 많기 때문에, UI 컬러 팔레트 또한 대체로 그레이 기반의 팔레트이다. (그나마 위의 ‘블루 그레이’처럼 브랜드 색이나 분위기에 맞는 틴트를 살짝 넣어서 개성을 더할 수는 있겠다) 

위의 그림을 잘 보면 다크 모드의 배경색 background-100이 완전한 검은색인 gray-100이 아닌 gray-095부터 시작하도록 되어 있다. 가장 밝은 색인 흰색과 실제 사용되는 가장 어두운 색의 간격을 약간이나마 줄여서, 보여지는 컨트라스트를 살짝 부드럽게 하려는 의도가 들어가 있다. (흰 배경 위에 텍스트색을 완전히 검은색이 아닌 살짝 밝기가 들어간 짙은 회색을 이용하는것도 그런 의도다.) 이러한 조정도 입맛에 따라 구현 도중에 얼마든지 할 수 있다. 변수 선언을 그냥 한 칸씩 내려 밀면 되기 때문이다.

이렇게 만든 라이트/다크 모드 두가지 컨텍스트에서 쓰일 수 있는 전경/배경 색 팔레트를 코드로 적어보자면 이렇게 된다.

// light theme
:root {
  --ui-color-background-100: #colors[gray-000]; // white
  --ui-color-background-090: #colors[gray-005];
  --ui-color-background-080: #colors[gray-010];
  --ui-color-background-070: #colors[gray-015];
  --ui-color-background-060: #colors[gray-020];
  --ui-color-background-050: #colors[gray-030];
  --ui-color-background-040: #colors[gray-040];
  --ui-color-background-030: #colors[gray-050];
  --ui-color-background-020: #colors[gray-060];
  --ui-color-background-010: #colors[gray-070];
  --ui-color-background-000: #colors[gray-080];

  --ui-color-foreground-100: #colors[gray-100]; // black
  --ui-color-foreground-090: #colors[gray-095];
  --ui-color-foreground-080: #colors[gray-090];
  --ui-color-foreground-070: #colors[gray-085];
  --ui-color-foreground-060: #colors[gray-080];
  --ui-color-foreground-050: #colors[gray-070];
  --ui-color-foreground-040: #colors[gray-060];
  --ui-color-foreground-030: #colors[gray-050];
  --ui-color-foreground-020: #colors[gray-040];
  --ui-color-foreground-010: #colors[gray-030];
  --ui-color-foreground-000: #colors[gray-020];
}

// dark theme
@media (prefers-color-scheme: dark) {
  :root {
    --ui-color-background-100: #colors[gray-100]; // black
    --ui-color-background-090: #colors[gray-095];
    --ui-color-background-080: #colors[gray-090];
    --ui-color-background-070: #colors[gray-085];
    --ui-color-background-060: #colors[gray-080];
    --ui-color-background-050: #colors[gray-070];
    --ui-color-background-040: #colors[gray-060];
    --ui-color-background-030: #colors[gray-050];
    --ui-color-background-020: #colors[gray-040];
    --ui-color-background-010: #colors[gray-030];
    --ui-color-background-000: #colors[gray-020];

    --ui-color-foreground-100: #colors[gray-000]; // white
    --ui-color-foreground-090: #colors[gray-005];
    --ui-color-foreground-080: #colors[gray-010];
    --ui-color-foreground-070: #colors[gray-015];
    --ui-color-foreground-060: #colors[gray-020];
    --ui-color-foreground-050: #colors[gray-030];
    --ui-color-foreground-040: #colors[gray-040];
    --ui-color-foreground-030: #colors[gray-050];
    --ui-color-foreground-020: #colors[gray-060];
    --ui-color-foreground-010: #colors[gray-070];
    --ui-color-foreground-000: #colors[gray-080];
  }
}

10단계의 색조 팔레트를 배경/전경 두가지로 나누고 라이트/다크모드까지 고려하니 좀 부담스러운 양의 코드처럼 느껴질 수도 있지만, 어지간한 규모의 프로젝트에서는 이렇게 하는 것이 훨씬 효율적이라고 장담할 수 있다. 다크모드를 구현한다고 할 때, 프로젝트 내 개별적으로 스타일링된 UI 요소가 22개 이상이라면 위의 방식이 코드 줄 수를 더 절약하는 방법이다.

정말로 코드 용량을 조금 더 줄이고 싶다면, 사실 배경/전경색 팔레트의 가장 옅은 색들 (000부터 030정도까지)은 배제해버려도 큰 상관은 없다. 그렇게 낮은 숫자로 내려가면 대체로 배경 색으로는 너무 진하고, 전경 색으로는 너무 옅어서 가독성 측면에서 실용성이 별로 없기 때문이다.

아무튼, 위에서 봤던 UI 예시를 다시 가져와 방금 만든 CSS 변수로 구현한다고 생각하면, 이런 식으로 색을 지정할 수 있겠다.

라이트 테마에서도 다크 테마에서도 각 부분이 참조하는 UI컬러 변수는 동일하다
:root {
  --page-bg-color: hsl(var(--ui-color-background-100));
  --header-text-color: hsl(var(--ui-color-foreground-080));
  --subheader-color: hsl(var(--ui-color-foreground-040));
  --paragraph-color: hsl(var(--ui-color-foreground-080));
  --callout-bg-color: hsl(var(--ui-color-background-080));
}

사실 어지간한 부분은 이렇게 각 UI 컴포넌트/부분별 변수를 추가적으로 선언해줄 필요도 없이, 그냥 각 부분의 색 선언에 곧바로 --ui-color 변수를 사용해도 된다.

body {
  color: hsl(var(--ui-color-foreground-080));
  background-color: hsl(var(--ui-color-background-100));
}

h1, p {
	color: hsl(var(--ui-color-foreground-080));
}

.callout {
	background-color: hsl(var(--ui-color-background-080));
	color: hsl(var(--ui-color-foreground-060));
}

select {
	border-color: hsl(var(--ui-color-foreground-020));
	background-color: hsl(var(--ui-color-background-100));
}

select:hover {
	border-color: hsl(var(--ui-color-foreground-060));
	background-color: hsl(var(--ui-color-background-090));
}

비로소 완전체에 도달한 모습이다. 여기까지 왔다면 이제 새로운 UI 컴포넌트나 페이지를 추가할 때 일일이 다크 모드를 위한 오버라이드 처리를 해야할 생각을 할 필요 없이, 해당 요소의 컨텍스트를 고려해 알맞은 전경/배경 색을 입혀주고, 라이트/다크 모드를 전환해가며 제대로 나오는지 테스트만 해주면 된다.


보너스: 투명도가 들어간 색 사용하기

디자인을 하다보면, 텍스트나 배경 색을 반투명하게 만들어야하는 상황이 올 수 있다. 물론 opacity 속성으로 통째로 반투명하게 만들수도 있겠지만, 텍스트는 불투명하게 하고 배경만 반투명하게 만들고 싶은 상황. 이럴 때 앞서 골랐던 HSL 포맷이 빛을 발한다.

현행 CSS3에서 투명도 즉 알파채널 값이 포함된 색을 표시할 방법은 여럿이 있다. 대표적으로 가장 먼저 등장해 널리 지원되는 건 rgba(255, 255, 255, 0.5)형식이다. RGB에 0~1 사이의 소숫점으로 표현한 알파값을 넣은 것. 앞서 우리가 사용했던 HSL 기반의 표기법에도 마찬가지로 뒤에 알파값을 허용하는 hsla(0%, 50%, 100%, 0.5)의 형식이 있다. 그리고 HEX 코드 형식에 알파값을 16진법으로 붙여 표기하는 #rrggbbaa 형식도 CSS 컬러모듈 레벨4의 스펙에 추가되어, 최신 브라우저들에서는 지원되고 있으나… 개인적으로는 앞서 이야기했듯이 역시 16진법의 값이 눈에 잘 익지 않아 rgba()hsla()를 선호하는 편이다.

팁: 참고로, 최신 스펙상으로는 rgb(),hsl()이라고 쓰고 안에 알파값을 옵션으로 넣어도 제대로 해석한다 (참고: rgb() 알파값 지원, hsl() 알파값 지원). 예를 들면, rgb(255, 255, 255, 0.5)도 올바른 신택스고 hsl(0%, 50%, 100%, 0.5)도 올바르다.

한 단계 더 나아가서, 콤마로 괄호 안의 값을 구분(함수형 구문)하는 것이 아닌 공백으로 구분하는 문법도 있다. 이 문법대로 쓰면, hsl(0% 50% 100%)처럼 적을 수 있다. 여기에 투명도를 더할 때는 슬래쉬 /를 쓰는데, 즉 hsl(0% 50% 100% / 0.5)의 형태가 된다. (관련 MDN 문서)

이러한 최신 문법은 모던 브라우저에서는 이미 널리 지원되고 있고, IE정도만 지원을 안 하는 정도다. 따라서 prefers-color-scheme등을 써 다크 모드를 지원하는 모던한 스타일시트를 구현하려 하는 거라면, 기왕 하는 김에 이런 모던한 문법의 색상표기법을 써보는게 더 미래지향적인 방법일수도 있겠다.

아무튼 그리하여 HSL를 기준으로 이야기하자면, 기존 색에 투명도를 더하려면 hsl() 괄호 안의 4번째 값으로 투명도 값을 넣어주면 된다. 그런데… 이미 CSS 변수화된 색에다가도 투명도를 더하는 것이 가능할까?

우선 CSS variable의 특징을 하나 짚고 넘어가자. CSS 변수는 스펙상, 값으로 어떤 문자열이 지정되어 있더라도, 변수를 불러온 자리에 그대로 대입한다.

:root {
  --my-var: asdf, sdfa;
}

.bar {
  border: 1px solid var(--my-var);
}

/* 실제 브라우저 해석 */
.bar {
  border: 1px solid asdf, sdfa;
}

(당연히 이걸 실제 브라우저에서 띄운다면 border 속성의 값이 잘못되었다고 판단해 무시될 것이다. 아무튼 요점은 이렇게 된다는 거다.)

:root {
  --gray-020: hsl(227, 17%, 77%);
}

.semi-transparent {
  background-color: var(--gray-020); // how to set 0.5 opacity to color?
}

위의 예시처럼 만약 색의 변수 자체에 hsl() 괄호 부분을 포함시켰더라면, 실제로 저 색을 사용할 때 투명도를 적용하기 난감했을 것이다. 변수에서 가져온 값은 이미 완성된 HSL 색이기 때문이다. 하지만 변수 자체에서는 괄호 안의 값만 남기고 색을 불러온 속성에서 hsl()을 입혀준다면, 해당 인스턴스에서 필요할 때 자유롭게 투명도 값을 추가할 수 있다.

:root {
  --gray-020: 227, 17%, 77%;
}

.semi-transparent {
  background-color: hsl(var(--gray-020),0.5);
}

바로 이렇게.

한 술 더 떠서, 투명도의 수치도 라이트 모드/다크 모드 각각 다르게 조정해야할 필요가 있다면 (상황에 따라 어느 한 쪽 테마에서만 너무 밝거나 어두울 수 있다) 그것 또한 가능하다.

:root {
  --navbar-background-opacity: 0.5;
}

@media (prefers-color-scheme: dark) {
  :root {
    --navbar-background-opacity: 0.8;
  }
}

.navbar {
  background-color: hsl(var(--ui-color-background-080),var(--navbar-background-opacity));
}

마치며

이로써 내가 하고싶은 이야기는 다 했다. 의도한 건 아니었지만 여기까지 오는데 생각보다 너무 많은 시간이 걸렸다. 글을 처음 쓰기 시작한건 무려 1년 전이었는데, 그간 내가 블로그에 워낙 소홀했던 것도 있었고… 이따금 글을 다시 완성해보려고 붙잡아봐도 처음에 작성하기 시작했던 내용을 다시 읽다 보면 내가 읽어도 너무 복잡해 보이길래 좀처럼 완성하기가 어려웠다. 어떻게하면 조금 더 알기 쉽게 설명할까 고민하다보니 밑밥을 깔기 위한 글의 앞 내용이 추가되면서 본론이 뒤로 밀리고 순서가 바뀌고, 흐름을 매끄럽게 하기 위해 고쳐쓰고 또 고쳐쓰고, 설명을 조금이나마 쉽게 하려고 예시 이미지도 만들어보고… 해서 여차저차 완성이랍시고 하긴 했지만 여전히 내가 기대했던 만큼 이해하기 쉽게 풀어내진 못 한 것 같다.

앞서 적었던 글과 이번 글에서 소개한 내용은, 현재 내가 재직중인 회사의 프로덕트에서 사용되고 있는 실제 컬러 시스템과 거의 흡사한 형태다. 회사 프로덕트(Shakr Studio)에 라이트/다크 전환가능한 테마를 런칭한지는 이제 반년이 조금 넘었는데(2020년 12월), 작년부터 여기저기 다른 회사들/사람들의 구현 방식을 들여다보면서 연구하고 우리의 실정에 맞게 이것저것 바꿔가며 테스트해보고 도달한 나름의 결론이다.

모든 규모의 프로젝트에 위와 같은 전략이 가장 최적의 구현 방법이라고 장담할 수는 없겠지만, 위의 생각 과정이 여러분의 프로젝트에 알맞은 시스템을 찾아가는데 조금이나마 인사이트가 될 수 있다면 그것만으로도 본 글의 역할은 충분히 했다고 본다.


글의 내용에 대해 궁금한 점이 있다면, 혹은 글의 내용중 잘못된 점이 있다면 댓글로 남겨주세요.