웹 다크모드 & 라이트모드, 개발팀과 디자인팀의 과정 (feat. Next.js , Tailwind)

안녕하세요. 이번 글에서는 Riido가 기존 다크모드의 서비스에 라이트모드를 추가하는 경험을 개발팀의 입장에서 공유하고자 합니다.
Riido에 라이트모드를 도입하게 된 배경
Riido는 출시 당시 다크모드만을 지원하는 서비스로 시작했습니다. 하지만 서비스가 성장하면서 다크모드를 자주 사용하는 개발자 뿐만 아니라, 디자이너와 같이 라이트모드를 선호하는 직군의 사용 비율이 점차 높아지며, 다양한 피드백을 받게 되었습니다.
“라이트모드를 선호하는데 선택권이 없어서 아쉬워요.”
이처럼 라이트모드에 대한 수요가 증가하고 있다는 점을 인지하여, 라이트모드 도입을 결정했고, 약 2주간의 스프린트가 시작되었습니다.
디자인 시스템과 협업 프로세스
1. 디자인 시스템
Riido 디자인팀은 새로운 색상 테마를 도입하며 여러 요소를 신중하게 고려했습니다. 그중 핵심적인 사항은 다음과 같습니다.
"협업 부분에서 라이트모드와 다크모드 grayscale 요소의 반전, 예외 사항에 대해서 전달하는 부분이 중요하다고 생각했습니다!"
- 배경색과 텍스트 색의 대비: 너무 극명한 대비는 눈의 피로를 유발할 수 있으므로, 적절한 대비를 유지하여 가독성을 높이는 것이 중요합니다.
- 화면 내 계층(레이어) 구조: 다크모드에서는 면의 밝기를 조절하여 계층을 나누고, 라이트모드에서는 컨테이너의 경계선이나 그림자를 활용하여 레이어를 구분합니다.
- 색상의 채도 및 명도 조절: 어두운 배경에서는 채도가 높은 색상이 번져 보일 수 있으므로 채도를 낮추고, 밝은 배경에서는 명도를 높여 배경과의 조화를 맞춥니다.
- 에셋 조정: 아이콘, 버튼 등의 에셋은 다크모드와 라이트모드에서 각각 다른 색감과 명도를 적용하여 자연스러운 사용자 경험을 제공합니다.
디자인팀의 라이트모드 제작 과정의 자세한 내용은 아래 글을 통해 확인 가능합니다.

2. 디자이너와 개발자 협업 과정
회의를 통해 논의되었던 핵심적 사항은 다음과 같습니다.
저희는 기존에 다크모드 색상만으로 스타일 관리 체계가 짜여 있었기 때문에 각 스타일에 대응하는 라이트모드 색상을 관리할 수 있도록 구조 변화가 있었습니다. 논의 후에 Tailwind config 커스텀 컬러에 dark와 light 계층을 추가하는 방법을 선택하여 직관적으로 관리할 수 있도록 했습니다.
서버에도 저장할지에 대한 고민이 있었고, API 요청을 줄이고자 클라이언트에서만 관리하는 방식을 채택했습니다.

기존에 라벨과 같은 특정 UI 요소들은 서버에서 컬러코드를 가져와 보여주는 부분이 있는데 이를 테마에 따라 다르게 보여주기 위한 방법을 논의했습니다.
최종적으로는 디자인팀에서 두가지 테마의 Color Palette를 만들었고, 서버에서는 다크/라이트에 해당하는 Hexcode 를 모두 반환해 주면 클라이언트는 사용자 테마에 맞게 보여주는 방식을 사용했습니다.
기술적 구현 과정
다크 모드와 라이트 모드에 따른 스타일은 Tailwind 클래스로 선언하고, Next.js , Redux 를 이용해 테마 상태를 관리했습니다.
1. Next.js에서의 멀티 테마 구현 방법 설정
구현을 위해 고려했던 방법으로는 몇 가지가 있었습니다. 1. next-themes
라이브러리 사용 2. LocalStorage 와 전역 상태 관리
를 활용한 구현 방법이 있었습니다. next-themes 는 SSR 로드 시의 깜빡임 현상을 개선한 이점이 있었지만 외부 의존성을 줄이고 자체적 해결을 위해 LocalStorage 와 ReduxToolkit을 활용한 방식을 선택했습니다.
📌 next-themes vs LocalStorage + 전역 상태 관리 비교
비교 항목 | next-themes |
LocalStorage + 전역 상태 관리 |
---|---|---|
설정 간편함 | ✅ 간편함 (설치 후 바로 사용 가능) | ❌ 직접 로직을 구현해야 함 |
자동 감지 (시스템 테마 반영) | ✅ 기본 제공 | ❌ 직접 감지 및 반영 로직 필요 |
Next.js 최적화 | ✅ SSR 친화적 (ex. useTheme) | ⚠️ 초기 로드 시 localStorage 접근으로 SSR 문제 가능 |
전역 상태 관리 | ✅ useTheme 훅 제공 | ✅ 커스텀 훅 + Zustand, Redux 등 활용 가능 |
커스텀 확장성 | ⚠️ 제한적 (next-themes의 기능 내에서 커스텀) | ✅ 완전한 커스텀 가능 (예: 다양한 테마 관리) |
브라우저 지원 | ✅ localStorage & prefers-color-scheme 자동 지원 | ✅ localStorage 사용 가능하지만, 브라우저 지원 직접 처리 필요 |
2. 테마 상태 관리 (ReduxToolkit 활용)
테마 상태를 전역적으로 관리 할 때 특히 중요했던 점은 사용자 설정 상태와 시스템 설정과의 동기화였습니다. 아래와 같은 구조를 통해 사용자가 설정 페이지에서 테마를 바꿀 때, 전체 컴포넌트에 전달 되도록 했습니다.
export const THEME_KEY = 'theme_mode'; // localStorage 저장 key 이름
export type ThemeMode = 'light' | 'dark' | 'system'; // 사용자 설정 테마 모드
export interface ThemeState {
mode: ThemeMode;
currentTheme: 'light' | 'dark';
isDarkMode: boolean;
}
// Theme Slice Helper 함수들
const getInitialThemeMode = (): ThemeMode => {
if (typeof window !== 'undefined') {
return (localStorage.getItem(THEME_KEY) as ThemeMode) || 'system';
}
return 'system';
};
const getInitialCurrentTheme = (): 'light' | 'dark' => {
if (typeof window === 'undefined') return 'dark';
const savedMode = localStorage.getItem(THEME_KEY);
if (savedMode === 'light' || savedMode === 'dark') {
return savedMode;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
};
// Redux Theme Slice
const initialState: ThemeState = {
mode: getInitialThemeMode(), // 사용자 설정 테마 'dark'|'system'|'light'
currentTheme: getInitialCurrentTheme() // 실제 적용된 테마 컬러
isDarkMode: getInitialCurrentTheme() === 'dark',
}
export const themeSlice = createSlice({
name: 'theme',
initialState,
reducers: {
setTheme: (state, action: PayloadAction<ThemeMode>) => {
state.mode = action.payload;
if (typeof window !== 'undefined') {
localStorage.setItem(THEME_KEY, action.payload);
if (action.payload === 'system') {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
state.currentTheme = isDark ? 'dark' : 'light';
state.isDarkMode = isDark;
} else {
state.currentTheme = action.payload;
state.isDarkMode = action.payload === 'dark';
}
}
},
// ... 기타 reducer 로직
},
});
3. 시스템 테마 연동과 실시간 감지
다음으로 사용자의 시스템 테마 변경을 실시간으로 감지하고 document에 적용하기 위해 ThemeProvider 컴포넌트를 구현하고, 앱의 전체를 감싸고 있는 Redux Provider 아래에 추가했습니다.
// ThemeProvider.tsx
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
const dispatch = useAppDispatch();
const { mode, currentTheme } = useAppSelector((state: RootState) => state.theme);
// 시스템 테마 변경 감지
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemThemeChange = () => {
const newTheme = mediaQuery.matches ? 'dark' : 'light';
dispatch(updateCurrentTheme(newTheme));
};
// 사용자 설정 테마 모드가 system 인 경우 전역 상태 업데이트
if (mode === 'system') {
handleSystemThemeChange();
mediaQuery.addEventListener('change', handleSystemThemeChange);
return () => mediaQuery.removeEventListener('change', handleSystemThemeChange);
}
}, [mode, dispatch]);
// Redux 변경 감지 후 Theme 적용
useEffect(() => {
const root = document.documentElement;
if (!root.classList.contains(currentTheme)) {
root.classList.remove('light', 'dark');
root.classList.add(currentTheme);
}
}, [currentTheme]);
return <>{children}</>;
}

라이트 모드 도입 후 느낀 점
1. UX , 성능, 유지보수성을 위해 고려할 점
1️⃣ 초기 로드 최적화를 위해 인라인 스크립트를 통한 초기 테마 적용으로 FOUC를 방지했습니다.
// FOUC 방지를 위해 적용한 코드
// next/script 태그가 아닌 일반 <script>태그를 사용해야 사전에 테마 정보에 접근할 수 있음
<script
dangerouslySetInnerHTML={{
__html: `
try {
const mode = localStorage.getItem('theme_mode') || 'system';
const root = document.documentElement;
root.classList.remove('light', 'dark');
if (mode === 'system') {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
root.classList.add(isDark ? 'dark' : 'light');
} else {
root.classList.add(mode);
}
} catch (e) {
console.error('Theme mode 설정 중 오류:', e);
}
`,
}}
/>
사전에 로컬스토리지에 접근하기 위한 스크립트
2️⃣ CSS 트랜지션을 활용한 부드러운 테마 전환을 통해 눈의 피로를 줄이고 UX를 개선할 수 있습니다.
3️⃣ 저희팀은 Tailwind 를 통해 스타일링을 하고 있습니다. Tailwind 로 쉽게 스타일링할 수 있지만 클래스명이 길어지는 단점이 있습니다. Dark 모드가 추가되면서 요소 하나에 적용하는 스타일 class 수가 기존보다 더욱 길어졌습니다. 일부 색상은 테마별 차이를 두지 않으면 관리해야 하는 색상이 줄어들어 유지보수가 쉬워집니다.
4️⃣ 두 가지 테마를 구현하게 되면서 다크 모드와 라이트 모드에서 반복되는 스타일 패턴을 발견할 수 있었고, 스타일 관리 방법을 더 효율적으로 개선했습니다.
5️⃣ 다크모드와 라이트 모드에서 이미지 리소스가 달라져야 하는 경우:
테마 변경 시 단순히 이미지를 바꾸면 깜빡임 현상이 발생할 수 있습니다. 대신 SVG를 사용하여 fill , stroke 등의 CSS 속성을 테마에 맞게 조절하는 것이 좋습니다.
2. QA 절차에서 중요했던 경험
1️⃣ 디자인시스템의 상황별(Normal, Hover, Selected, Disabled) UI 케이스가 있는 경우 새로운 테마에서도 대응되는 UI 체크는 필수
UI 컴포넌트들이 디자인과 프론트 측에서 동일하게 잘 관리되는 것이 중요하다고 느꼈던 포인트입니다. 첫 QA 때 케이스별로 누락된 스타일이 많아 열심히 추가했던 기억이 납니다. 기존에 컴포넌트로 관리되지 않았던 요소들도 보완하게 된 포인트입니다.
2️⃣ 다크 모드에선 보이지 않던 ‘선’의 존재감
라이트 모드에서는 UI 의 선의 두께와 색상 차이가 더욱 극명하게 보입니다. 이전에 보이지 않던 선 UI 개선 QA 들이 있었습니다.
3️⃣ QA 기간 중 색상 변경이 여러 번 이루어짐
실제 Preview 배포 후에 ‘요소의 깊이감’, ‘색상 통일성’, ‘회색 계열 사용 시 문제’ 등으로 인해 다수의 색상 변경이 있었습니다. 이점을 고려해서 연관된 요소들은 한 번의 Hexcode 변경만으로 쉽게 작업이 이루어지도록 사전에 대비하면 좋습니다.
결과 및 향후 계획
테마변경 이벤트를 집계하고 사용자 행동을 분석한 결과, 배포 후 라이트 모드도 다크모드만큼 많은 사용자들이 사용하고 있었고, “라이트 모드 UI 가 생기니 답답한 느낌이 덜해서 더 좋습니다”라는 피드백도 받게 되었습니다.

앞으로도 사용자 피드백을 바탕으로 지속적인 서비스 개선을 진행할 예정입니다. 마지막으로 구현 완료된 모습입니다.

신규 기능을 사용자에게 잘 전달하기 위해 사이드 네비게이션 하단에도 테마 전환 버튼을 추가했습니다.

이상으로 Riido의 라이트모드 도입기였습니다.
여러분은 어떤 테마를 선호하시나요?
아래 링크를 통해 뤼이도의 다크모드와 라이트모드를 직접 확인할 수 있습니다.
Riido Frontend Developer 고희주
