1. 기능 정의
1. 목적
tailwindcss로 다크모드를 설정한다.
2. 주요 기능 목록
일반모드/다크모드 각각 색상을 미리 설정하고, 특정 색상으로 설정하면 둘 다 적용된다.
2. 스펙 정의
1. 기술 스택
tailwindcss
3. 개발 일정
- 시작일: 7/26
- 종료 예정일: 7/27
4. 사전 조사
1. 기본적인 tailwindcss darkMode
tailwindcss에서는 기본적으로 darkMode를 지원한다. 사용 방법은 다음과 같다.
<div class="dark:bg-black bg-white">
{/* .. */}
</div>
위와 같이 작성하면, 평소에는 배경색이 white, 다크 모드가 되면 black이 적용되는 방식이다.
이를 위해서는 추가 설정이 필요하다.
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
// ...
}
darkMode 설정에는 class와 media가 있으며, 직접 토글링하기 위해서는 class로 설정하면 된다.
그리고, 루트에서 dark라는 class를 넣어주면 된다.
<!-- Dark mode enabled -->
<html class="dark">
<body>
<!-- Will be black -->
<div class="bg-white dark:bg-black">
<!-- ... -->
</div>
</body>
</html>
다크모드를 온/오프하기 위해서는 아래와 같이 할 수 있을 것이다.
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
add, contains, keys 등 다양한 class를 조작할 수 있는 메서드를 제공한다.
![]()
document의 루트 요소를 반환한다. HTML 문서에서는 html태그가 이에 해당한다.
![]()
단점
처음에는 다크모드를 위와 같이 적용했지만, 상당히 불편함을 느꼈다. 모든 컴포넌트에 다크모드를 지원하기 위해서는 모든 컴포넌트의 배경색과 글자색, hover가 있다면 hover도 각 색상을 지정하고, 다크모드에 대한 색상도 지정해야 한다는 점이다.
이럴 경우 기본색 - 다크모드 색에 대한 불일치가 생길 수 있고, 신경 써야 하는 점이 많아진다. 이러한 점을 일괄적으로 처리할 수는 없을까?
2. tailwindcss darkMode 간편하게 사용하기
tailwindcss의 CSS Variables를 활용한 방법이다.
-
tailwind.config.ts에서 변수명 설정
const config = { theme: { extend: { colors: { grape: "rgba(var(--grape))", } } } } -
globals.css에서 변수명 - 색상 설정
:root { --grape: // .. } .dark { --grape: // .. } .red { --grape: // .. }
위와 같이 설정할 경우, bg-grape라 설정할 경우, 루트에 아무 것도 없다면:root 내부 속성이, 루트에 dark가 설정되어 있을 경우 .dark 내부 속성이 적용된다.
3. CSS Variables
-를 변수명 앞에 붙여주면, CSS 변수를 사용할 수 있다.
-
커스텀 CSS 속성을 지정한다.
// style.css section { --main-bg-color: brown; } :root { --main-bg-color: brown; }트리의 루트 요소를 가리킨다. 즉, html태그와 동일하다.
위와 같이 특정 태그 혹은 루트 요소에 변수를 설정할 수 있다. 위의 section과 :root(즉 html)은 css 변수를 사용할 수 있는 스코프가 된다.
// style.css @property --logo-color { syntax: "<color>"; inherits: false; initial-value: #c0ffee; }위와 같이
@property를 이용해서 선언할 수도 있다. -
커스텀 CSS 속성을 사용한다.
// style.css details { background-color: var(--main-bg-color); }위와 같이
var(변수명)을 이용해서 사용할 수도 있다.
즉, 2번과 3번처럼 CSS 변수를 사용해서 색상을 한 가지로 지정하여, 다크 모드 / 라이트 모드 등 각 모드에 따른 색은 하나의 css 파일에서 관리하고자 한다.
5. 개발
1. 각 모드에 맞는 색상을 지정한다.
// globalcss
:root {
/* 라이트 모드 기본 색상 */
--white-1: #F5F5F5;
--white-2: #FFFFFF;
--black-1: #212121;
--black-2: #000000;
--gray-1: #F5F5F9;
--gray-2: #E0E0E0;
--gray-3: #BDBDBD;
}
.dark {
/* 다크 모드 기본 색상 */
--white-1: #212121;
--white-2: #424242;
--black-1: #F5F5F5;
--black-2: #FFFFFF;
--gray-1: #333333;
--gray-2: #424242;
--gray-3: #616161;
}
:root는 html태그와 동일하며, 기본 설정(라이트 모드)이다.
2. tailwind.config에서 각 색상을 CSS 변수로 지정한다.
// tailwind.config.ts
module.exports = {
presets: {
theme: {
colors: {
'white-1': 'var(--white-1)',
'white-2': 'var(--white-2)',
'black-1': 'var(--black-1)',
'black-2': 'var(--black-2)',
'gray-1': 'var(--gray-1)',
'gray-2': 'var(--gray-2)',
'gray-3': 'var(--gray-3)',
}
}
}
}
tailwindCSS의 커스텀 스타일과 CSS 변수를 활용한 것이다.
3. tailwindCSS의 dark모드를 활성화한다.
// tailwind.config.ts
module.exports = {
darkMode: 'class',
};
위와 같이 darkMode를 class로 설정할 경우, ‘dark’라는 스타일이 지정된 태그의 모든 하위 태그에는 dark스타일이 적용된다. 공식문서
4. CSS 변수를 사용한다.
이제, bg-white-1, text-black-1 등 커스텀 CSS 변수를 className에 지정하면 다크모드가 적용된다.
6. 추가 개발
1. 다크 모드 적용
다크 모드를 원활하게 적용하기 위해서 고려해야 할 점은 4가지 정도가 있다.
- 서비스의 전반적인 테마가 바뀌어야 한다.
- 사용자가 새로고침을 하거나, 재접속을 해도 이전 테마가 적용된다.
- 사용자가 기본적으로 설정한 시스템의 테마로 초기 접속 시 설정한다.
1번은 위에서 개발한 css 변수를 활용하면 된다. html에 dark를 추가하면 하위 모든 태그에서 dark 스타일이 적용된다.
document.documentElement.classList.add(CLASS_NAME_DARK); // 다크 모드
document.documentElement.classList.remove(CLASS_NAME_DARK); // 라이트 모드
2번은 쿠키를 활용하였다. 쿠키는 지정한 expire times만큼 상태가 유지되고, 서버에서도 접근 가능하기에 SSR이 가능하다는 장점이 있다.
// layout.tsx
<html class={`{theme === CLASS_NAME_DARK && 'dark'}`}>
// ..
</html>
사용자가 재접속했을 경우, 쿠키에 저장된 테마로 결정한다.
3번은 window.matchMedia를 활용하였다. window.matchMedia는 주어진 미디어 쿼리 문자열의 분석 결과를 나타내는 MediaQueryList 객체를 반환한다.
const isUserPreferColorSchemaDark = window.matchMedia(
'(prefers-color-scheme:dark)'
)?.matches;
if (isUserPreferColorSchemaDark) makeThemeDark();
else makeThemeLight();
그리고 마지막으로, 사용자에게 현재 다크 모드인지/라이트모드인지 아이콘으로 보여주어야 한다. 이를 위해서는 컴포넌트를 리렌더링해야 한다. 그래서 테마를 변경하는 컴포넌트에 상태값을 만들어 컴포넌트 내부에서만 상태 관리하도록 하였다.
setTheme(Theme.DARK);
setTheme(Theme.LIGHT);
전체 코드는 다음과 같다.
ToggleTheme.tsx
'use client';
import MoonIcon from '#images/icons/moon.svg';
import SunIcon from '#images/icons/sun.svg';
import { CLASS_NAME_DARK, COOKIE_KEY_THEME, Theme } from '@constants';
import { setCookie } from '@guesung/utils';
import { useEffect, useState } from 'react';
interface ToggleThemeProps {
initialTheme: Theme;
}
export default function ToggleTheme({ initialTheme }: ToggleThemeProps) {
const [theme, setTheme] = useState(initialTheme);
const makeThemeDark = () => {
setCookie(COOKIE_KEY_THEME, Theme.DARK);
setTheme(Theme.DARK);
document.documentElement.classList.add(CLASS_NAME_DARK);
};
const makeThemeLight = () => {
setCookie(COKIE_KEY_THEME, Theme.LIGHT); // 새로고침 시 테마 보존
setTheme(Theme.LIGHT); // 컴포넌트 리렌더링
document.documentElement.classList.remove(CLASS_NAME_DARK); // html에 테마 적용
};
// 초기 테마 설정
useEffect(() => {
if (initialTheme === undefined) {
const isUserPreferColorSchemaDark = window.matchMedia(
'(prefers-color-scheme:dark)'
)?.matches;
if (isUserPreferColorSchemaDark) makeThemeDark();
else makeThemeLight();
}
}, []);
const handleTheme = () => {
if (theme === Theme.DARK) makeThemeLight();
else makeThemeDark();
};
return (
<button onClick={handleTheme} aria-label="dark mode toggle">
{theme === Theme.DARK && <SunIcon fill="white" width={16} height={16} />}
{theme === Theme.LIGHT && <MoonIcon width={16} height={16} />}
</button>
);
}
2. transition 적용
CSS에는 transition이라는 프로퍼티가 존재한다. 스타일이 보다 자연스럽게 바꿔주는 속성이다.
그리고, tailwinCSS에는 이런 transition을 간편하게 사용할 수 있도록 유틸리티로 제공한다.
// layout.tsx
<html class={`{theme === CLASS_NAME_DARK && 'dark'} transition-colors`}>
// ..
</html>
위 transition-colors의 CSS 프로퍼티는 다음과 같다.
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;