다크모드 토글 포트폴리오 페이지 만들기
왜 만들었나
CSS 변수와 localStorage만으로 다크/라이트 모드 전환을 깔끔하게 구현할 수 있다. 프레임워크 없이 바닐라 JS 한 파일로 완성하는 포트폴리오 페이지를 만들어봤다.
목표는 세 가지였다.
- CSS custom properties로 테마 전환 — JS에서 클래스명 토글 하나로 전체 색상 변경
prefers-color-scheme으로 시스템 설정 감지 — 첫 방문자에게 맞는 테마 제공localStorage로 사용자 선택 저장 — 새로고침해도 유지
구조
파일 하나짜리 단일 HTML. 외부 의존성 없음.
05-darkmode-portfolio/
└── index.html전체 구조는 간단하다.
| 섹션 | 내용 |
|---|---|
| Nav | sticky 헤더, 테마 토글 버튼 |
| Hero | 인트로 텍스트, CTA 버튼 |
| About | 4개 카드 (그리드 레이아웃) |
| Skills | 태그 그룹 (Frontend / Backend / Tools) |
| Projects | 프로젝트 카드 목록 |
| Contact | 연락처 박스 |
핵심 구현
CSS 변수로 테마 분리
[data-theme="dark"] {
--bg: #0f1117;
--surface: #1a1d27;
--text: #e2e8f0;
--accent: #6366f1;
--tag-bg: rgba(99,102,241,0.12);
--toggle-bg: rgba(255,255,255,0.08);
}
[data-theme="light"] {
--bg: #f8fafc;
--surface: #ffffff;
--text: #0f172a;
--accent: #4f46e5;
--tag-bg: rgba(79,70,229,0.08);
--toggle-bg: rgba(0,0,0,0.06);
}
<html data-theme="dark"> 이 속성 하나만 바꾸면 변수값이 전부 교체된다. 컴포넌트마다 클래스를 토글할 필요가 없다.
전환 애니메이션은 body에 transition 하나로 해결한다.
body {
transition: background 0.25s ease, color 0.25s ease;
}
시스템 설정 감지 + localStorage 우선순위
const saved = localStorage.getItem('theme');
const system = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark' : 'light';
const initial = saved || system;
applyTheme(initial);
우선순위:
| 순위 | 출처 | 설명 |
|---|---|---|
| 1 | localStorage |
사용자가 직접 선택한 값 |
| 2 | prefers-color-scheme |
OS/브라우저 시스템 설정 |
| 3 | HTML 기본값 | data-theme="dark" fallback |
saved || system 한 줄이 전부다. 저장값이 없으면 시스템 설정을 따른다.
토글 함수
btn.addEventListener('click', function () {
const next = html.dataset.theme === 'dark' ? 'light' : 'dark';
applyTheme(next);
localStorage.setItem('theme', next);
});
function applyTheme(theme) {
html.dataset.theme = theme;
btn.textContent = theme === 'dark' ? '🌙' : '☀️';
}
현재 테마를 읽고, 반대값으로 바꾸고, 저장한다. 총 3줄.
sticky nav + backdrop blur
nav {
position: sticky;
top: 0;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
background: color-mix(in srgb, var(--bg) 80%, transparent);
}
color-mix()로 배경색 80% 불투명도를 만들었다. 테마가 바뀌어도 CSS 변수 기반이라 자동으로 따라온다.
비교 테이블
다크모드 구현 방식 비교.
| 방식 | 코드 복잡도 | 유지보수 | 추천 |
|---|---|---|---|
data-theme + CSS 변수 |
낮음 | 높음 | ✅ 이 방법 |
class 토글 (dark-mode) |
중간 | 중간 | |
| JS로 직접 style 변경 | 높음 | 낮음 | ❌ |
Tailwind dark: |
낮음 | 높음 | (빌드 필요) |
CSS 변수 방식은 변수 정의만 테마별로 분리하면 된다. 컴포넌트 코드는 건드릴 필요가 없다.
삽질 기록
color-mix() 브라우저 지원
color-mix(in srgb, var(--bg) 80%, transparent)가 Safari 16.2 미만에서 지원 안 된다. 단순 rgba fallback을 함께 두는 게 안전하다.
테마 깜빡임 (FOUC)
<script>를 <head> 안에 인라인으로 넣지 않으면 HTML 렌더링 후 JS가 실행되면서 테마가 잠깐 바뀌는 깜빡임이 생긴다. <body> 맨 위에 즉시 실행 함수(IIFE)로 테마를 먼저 적용하면 해결된다.
<html data-theme="dark"> <!-- fallback -->
<head>
<script>
// 렌더링 전에 즉시 실행
const t = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.dataset.theme = t;
</script>
prefers-color-scheme 변경 감지
사용자가 OS 테마를 바꿔도 localStorage에 저장된 값이 있으면 무시된다. 의도된 동작이지만, 만약 시스템 변경을 따르게 하고 싶다면 matchMedia().addEventListener('change', ...)를 추가하면 된다.
마무리
핵심은 CSS 변수 분리다. 테마 로직 자체는 JS 20줄이 전부고, 나머지는 전부 CSS 문제다.
변수를 잘 설계해두면 나중에 테마를 추가할 때도 변수 블록만 하나 더 추가하면 된다.
기술 스택: HTML · CSS Custom Properties · Vanilla JS · localStorage
소스 코드: GitHub
데모: GitHub Pages
'튜토리얼 > 웹' 카테고리의 다른 글
| [HTML/CSS/JS] Canvas API로 그림판 만들기 (0) | 2026.03.01 |
|---|---|
| [HTML/CSS/JS] 로컬스토리지 메모장 앱 만들기 (0) | 2026.02.28 |
| [Next.js] Supabase로 방명록 만들기 (풀스택) (0) | 2026.02.27 |
| [React] 실시간 환율 계산기 만들기 (API 연동) (0) | 2026.02.27 |
| [HTML/CSS/JS] 드래그앤드롭 Todo 앱 만들기 (0) | 2026.02.26 |