다크모드 구현의 함정들
다크모드 토글 하나 넣는 건 쉬운데, 제대로 구현하는 건 생각보다 까다롭다
밤에 내 블로그를 열었더니 눈이 아팠다
그게 시작이었다. "토글 버튼 하나 넣으면 되지." CSS 변수 몇 개 바꾸고 버튼 달면 끝날 줄 알았다. 토글 버튼 자체는 진짜 30분이면 만든다. 근데 "제대로 된" 다크모드를 만드는 데 일주일이 걸렸다. 일주일.
그 과정에서 밟은 함정들을 적어둔다.
함정 1: 새로고침하면 눈이 번쩍
첫 번째이자 가장 고통스러운 함정. FOUC(Flash of Unstyled Content). 다크모드를 설정해놨는데, 페이지 새로고침하면 흰 화면이 번쩍 나타났다가 어두워진다. 서버에서 렌더링할 때 사용자 테마 설정을 모르니까 기본값(라이트)으로 HTML을 보내고, JS가 실행된 뒤에 localStorage에서 설정을 읽어서 바꾸는 거다. 이 시간차 동안 깜빡임이 생긴다.
해결은 <head>에 인라인 스크립트 넣어서 JS 실행 전에 <html> 태그에 클래스를 추가하는 거다. 렌더링 블로킹이 되지만 코드가 아주 작아서 성능 영향은 무시할 만하다. 이걸 알아내는 데 반나절 걸렸다. (스택오버플로우 링크를 찾았는데 저장을 안 했다.)
함정 2: 시스템 설정이라는 변수
prefers-color-scheme으로 시스템 설정 감지하면 되는 줄 알았다. 근데 사용자 수동 선택까지 고려하면 상태가 3개다. 라이트, 다크, 시스템. 사용자가 수동으로 다크를 골랐으면 시스템이 라이트여도 다크 유지해야 하고, "시스템"을 골랐으면 시스템 설정이 바뀔 때마다 테마가 따라가야 한다. 이 로직을 useSyncExternalStore로 구현하는 데 꽤 시간이 걸렸다.
함정 3: 순수 검정 배경은 눈이 아프다
"배경을 까맣게 하면 되는 거 아닌가?" 순수 검정(#000000)에 순수 흰색(#FFFFFF) 텍스트를 넣으면 대비가 너무 강해서 오히려 눈이 피로하다. 다크모드 배경은 #121212~#1a1a1a 정도가 좋고, 텍스트는 #e0e0e0~#ebebeb 정도가 편하다. 다크모드는 "색을 반전하는 것"이 아니라 "어두운 환경에 맞게 재설계하는 것"이다. 이걸 몰라서 처음에 눈 아픈 다크모드를 만들어버렸다.
함정 4: 이미지가 눈을 찌른다
다크모드에서 흰 배경 이미지가 문제다. 스크린샷이나 다이어그램 같은 거. 살짝 투명도를 주거나 둥근 모서리에 테두리를 추가해서 배경이랑 어울리게 해야 한다.
그림자도 다시 생각해야 한다. 라이트모드의 box-shadow는 다크모드에서 안 보인다. 어두운 배경 위의 그림자니까. 약간 밝은 테두리를 쓰거나, background-color 차이로 높낮이를 표현해야 한다.
함정 5: 서드파티가 안 따라온다
Giscus 댓글 위젯이 별도 iframe이라 메인 테마가 바뀌어도 그 안은 그대로다. postMessage로 테마를 동적으로 전달해야 한다. 코드 하이라이팅도 마찬가지다. Shiki 다크/라이트 테마 둘 다 준비해놓고 CSS 변수로 전환해야 한다. 처음에 이걸 몰라서 다크모드에서 코드 블록만 환하게 빛나는 참사가 벌어졌다. (스크린샷 찍어둘 걸 그랬다.)
테스트 체크리스트를 만들었다
토글 눌러보는 것만으로는 부족하다. 새로고침했을 때 깜빡이지 않는지, 시스템 설정 바꿨을 때 따라가는지, 모든 페이지에서 텍스트 가독성 있는지, 이미지랑 코드 블록이 어울리는지, 서드파티 위젯이 연동되는지. 이걸 페이지마다 돌리면 빠뜨린 부분이 꼭 나온다.
일주일이 아깝진 않았다
다크모드는 CSS, JS, UX 디자인, 서드파티 연동까지 전부 건드려야 하는 작업이다. "쉬운 기능"처럼 보여서 함정이다. 근데 밤에 블로그 들어갔을 때 눈이 편해진 걸 느끼면, 일주일 투자가 아깝지 않다. 아마도.