67.1 고른 테마가 왜 새로고침하면 사라져요?
앞 편까지 왔으면 이제 버튼으로 켜고, OS도 따라가요. 그런데 여기 허탈한 함정이 하나 있어요. 사용자가 애써 다크로 바꿔놨는데, 페이지를 새로고침하거나 다른 글로 넘어가면 다시 라이트로 돌아가 있어요. 저도 이거 처음엔 버그인 줄 알았어요.
버그가 아니에요. 우리가 data-theme을 html에 붙였을 뿐, 그 선택을 어디에도 적어두지 않았거든요. 페이지가 새로 뜨면 html도 처음 상태로 리셋되니, 방금 고른 값은 흔적도 없이 사라져요. 그래서 오늘 할 일은 두 가지예요. 사용자 선택을 저장하는 것, 그리고 다음에 올 때 깜빡임 없이 되살리는 것이에요.
67.2 사용자 선택은 어디에 저장해요?
사용자의 테마 취향처럼 이 브라우저에 오래 남겨두고 싶은 작은 값은 localStorage(로컬 스토리지, 브라우저에 남는 저장소)에 담아요. 서버까지 갈 필요 없이 브라우저 안에 보관되고, 탭을 닫아도 지워지지 않아요. 쓰는 법도 간단해요. 이름과 값의 짝으로 넣고 빼요.
상황: 사용자가 테마를 고르면 그 값을 브라우저에 적어둬요.
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme); // 다음 방문 때 쓰려고 저장
}
setItem은 'theme'이라는 이름표에 값을 적어두는 거예요. 이제 사용자가 다크를 고르면 화면도 바뀌고, 그 선택도 저장돼요. 다음에 페이지가 뜰 때 그 값을 도로 꺼내 쓰면, 고른 테마가 그대로 유지돼요. 꺼낼 땐 getItem을 써요.
상황: 페이지가 뜰 때 저장해둔 값을 꺼내, 앞 편의 우선순위 로직에 넘겨요.
const saved = localStorage.getItem('theme'); // 없으면 null
const theme = resolveTheme(saved); // 66편에서 만든 판단 함수
document.documentElement.setAttribute('data-theme', theme);
저장된 게 없으면 getItem은 null을 돌려줘요. 그럼 앞 편의 resolveTheme이 OS 취향으로 떨어지고, 저장된 값이 있으면 그 선택을 우선 살려요. 저장과 앞 편의 우선순위 판단이 이렇게 맞물려 돌아가요.
67.3 그런데 왜 화면이 하얗게 번쩍여요?
여기까지 하면 기능은 다 됐어요. 그런데 다크로 저장해둔 사용자가 새로고침하면, 아주 짧게 하얀 화면이 번쩍 하고 지나간 뒤 어두워지는 게 보여요. 눈에 콕 박히는 거슬리는 깜빡임이죠. 이걸 FOUC(Flash Of Unstyled Content, 스타일 입혀지기 전 내용이 잠깐 보이는 현상)라고 불러요.
왜 이럴까요? 브라우저가 페이지를 위에서 아래로 읽는데, HTML 본문이 먼저 그려지고 나서야 맨 아래에 있던 자바스크립트가 실행돼 data-theme을 붙이기 때문이에요. 그 찰나의 틈에 화면은 기본값인 하얀 상태로 한 번 그려졌다가, 뒤늦게 어두워지는 거죠. 기능이 아니라 순서의 문제예요.
그러니 해법도 순서예요. 테마를 정하는 코드를 본문이 그려지기 전에, 그러니까 head 안에서 가장 먼저 실행하면 돼요. 화면이 단 한 번도 틀린 색으로 안 그려지게 앞당기는 거예요.
67.4 깜빡임은 어떻게 없애요?
방법은 head 안에 작은 인라인 스크립트를 넣는 거예요. 외부 파일로 부르면 다운로드를 기다리느라 또 늦으니, HTML에 직접 박은 짧은 코드로 넣어요. 이 조각이 본문보다 먼저 실행돼서, 첫 페인트 전에 data-theme을 붙여버려요.
상황: head 맨 위에 넣어, 본문이 그려지기 전에 테마를 먼저 확정해요.
<head>
<script>
(function () {
var saved = localStorage.getItem('theme');
var dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
var theme = saved ? saved : (dark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
</head>
이 스크립트는 본문(body)이 그려지기도 전에 실행돼요. 저장된 값이 있으면 그걸, 없으면 OS 취향을 읽어서 html에 data-theme을 곧바로 붙이죠. 그래서 브라우저가 첫 화면을 그리는 그 순간 이미 올바른 테마가 정해져 있어요. 하얀 번쩍임이 일어날 틈 자체가 사라지는 거예요.
한 가지 주의할 점이 있어요. 이 조각은 페이지에서 가장 먼저 도는 코드라서, 무겁거나 실수가 있으면 안 돼요. 딱 테마 확정에 필요한 최소한만 넣고, 나머지 로직은 평소처럼 아래쪽 스크립트에 두세요. 이 작은 조각 하나가 다크모드의 완성도를 확 끌어올려요. 있고 없고의 차이가 커요.
67.5 라이트, 다크 말고 시스템 따름은요?
여기까지면 거의 다 왔어요. 마지막으로 완성도를 높이는 세 번째 선택지 얘기예요. 잘 만든 다크모드 토글은 대개 라이트, 다크, 시스템 따름 이렇게 세 갈래예요. 앞의 둘은 고정이고, 시스템 따름은 "내가 정하지 않을 테니 OS를 따라가라"는 뜻이에요.
그래서 저장하는 값도 세 가지가 돼요. light, dark, system이죠. system이 저장돼 있으면 OS 취향을 그때그때 읽어 화면에 반영해요. 앞 편의 판단 함수를 이 세 갈래에 맞게 넓히면 이렇게 돼요.
상황: 저장값이 system이면 OS를 따르고, light나 dark면 그 값으로 고정해요.
function applyTheme(choice) {
var theme = choice;
if (choice === 'system' || !choice) {
var dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
theme = dark ? 'dark' : 'light';
}
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', choice); // 고른 갈래 자체를 저장
}
여기서 눈여겨볼 건, 저장은 사용자가 고른 갈래(choice)를 그대로 한다는 점이에요. system을 골랐으면 system이라고 저장하지, 계산된 dark를 저장하지 않아요. 그래야 다음에 와도 여전히 OS를 따라가거든요. 만약 dark로 저장해버리면, 밤에 골랐던 게 낮에도 다크로 굳어 시스템 따름의 의미가 사라져요.
그리고 system을 고른 동안엔 앞 편의 change 구독을 살려둬야, OS가 낮밤으로 바뀔 때 화면도 따라가요. 반대로 사용자가 light나 dark로 고정하면 그 구독은 무시하면 되고요. 이 세 갈래 관리까지 얹으면 다크모드가 흔한 앱들만큼 매끄러워져요.
67.6 오늘 배운 저장과 전환을 정리해볼게요
다크모드의 마지막 조각을 맞췄어요. 고른 테마가 남고, 다음에 와도 깜빡임 없이 살아나게요.
사용자 선택은 localStorage에 setItem으로 저장하고, 다음 방문 때 getItem으로 꺼내 되살려요. 저장된 게 없으면 앞 편의 판단이 OS 취향으로 떨어지고요. 새로고침 때 하얗게 번쩍이는 FOUC는, 테마 확정 코드를 head 안 인라인 스크립트로 옮겨 본문이 그려지기 전에 data-theme을 붙이면 사라져요. 그리고 라이트, 다크, 시스템 따름 세 갈래를 둘 땐, 고른 갈래 자체를 저장하고 system일 땐 OS 변화를 구독하는 게 요령이에요.
세 편을 지나며 다크모드를 바닥부터 세웠어요. 변수로 색을 모으고, OS 취향과 손잡고, 선택을 저장해 깜빡임 없이 되살렸죠. 돌아보면 어느 하나도 대단한 마법이 아니었어요. 색을 한곳에 모으고, 사용자 취향을 조용히 읽어주고, 고른 걸 잊지 않고 기억해주는 작은 배려의 합이었죠. 그 작은 것들이 쌓여서, 눈이 편안한 화면이 만들어져요.