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을 돌려줘요. 그럼 앞 편의 resolveThemeOS 취향으로 떨어지고, 저장된 값이 있으면 그 선택을 우선 살려요. 저장과 앞 편의 우선순위 판단이 이렇게 맞물려 돌아가요.

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 오늘 배운 저장과 전환을 정리해볼게요

다크모드의 마지막 조각을 맞췄어요. 고른 테마가 남고, 다음에 와도 깜빡임 없이 살아나게요.


사용자 선택은 localStoragesetItem으로 저장하고, 다음 방문 때 getItem으로 꺼내 되살려요. 저장된 게 없으면 앞 편의 판단이 OS 취향으로 떨어지고요. 새로고침 때 하얗게 번쩍이는 FOUC는, 테마 확정 코드를 head 안 인라인 스크립트로 옮겨 본문이 그려지기 전에 data-theme을 붙이면 사라져요. 그리고 라이트, 다크, 시스템 따름 세 갈래를 둘 땐, 고른 갈래 자체를 저장하고 system일 땐 OS 변화를 구독하는 게 요령이에요.


세 편을 지나며 다크모드를 바닥부터 세웠어요. 변수로 색을 모으고, OS 취향과 손잡고, 선택을 저장해 깜빡임 없이 되살렸죠. 돌아보면 어느 하나도 대단한 마법이 아니었어요. 색을 한곳에 모으고, 사용자 취향을 조용히 읽어주고, 고른 걸 잊지 않고 기억해주는 작은 배려의 합이었죠. 그 작은 것들이 쌓여서, 눈이 편안한 화면이 만들어져요.