💻 시작하며
요즘 개발하는 모던 웹 페이지에서 테마 기능을 빼놓을 수 없다.
따라서 새로운 프로젝트에서도 테마 기능을 넣어보려고 한다.
테마 정보를 어디에 기록하면 좋을까 생각하다가
여러가지 이유로 글 제목처럼 피니아 스토어에 기록해야겠다는 생각이 들었다.
🛠️ 요구사항
테마 기능을 구현할때 단순히 브라우저나 기기의 설정을 따라가기만 해도 좋지만
그와는 별도로 사용자 지정도 가능하게 하고 싶어서 요구사항을 비교적 복잡하게 지정했다.
- 기본적으로 기기의 설정에 따라 설정되어야 함
- 사용자가 테마를 변경시 기기의 설정과 별개로 동작해야 함
- 테마 정보는 저장되어 있다가 다시 접근할때 복원되어야 함.
- 테마를 변경할때 새로고침 등의 액션 없이 즉각적으로 반영되어야 함. ← 피니아를 사용하게 된 이유다.
- 라이트 / 다크 모드 뿐만 아니라 그외의 테마도 지원되어야 함 (예를 들면 true-dark 같은거)
⌨️ 각 기능 구현
이제 요구사항에 맞게 각각의 기능을 구현해보려고 한다.
❕기본적으로 기기의 설정에 따라 설정되어야 함
이 기능을 구현하는건 여러가지 방법이 있다.
대표적으로는 css의 미디어 쿼리를 활용하는 방법이 있다.
바로 아래 코드처럼 `prefers-color-scheme` 속성을 활용하는 방법이다.
기기 설정에 따라 서로 다른 테마를 지정할 수 있다.
다만 이 방법을 사용하면 사용자가 테마를 변경할 수 없기 때문에 두번째 요구사항이 충족될 수 없다.
따라서 이 속성은 단순히 현재 기기의 설정값을 불러오는데만 사용하기로 했다.
/**
* 현재 시스템 설정에 기반한 테마 값을 반환하는 함수
*/
const getSystemTheme = (): Theme => {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
그런데 만약 사용자가 웹사이트를 열어놓은 상태에서 기기 설정을 변경하게 된다면 어떻게 될까?
위 코드는 한번만 불러오는 함수기 때문에 기기 설정이 바뀌었을때 재호출을 하지 않는다면 현재 테마 값을 즉시 불러올 수 없다.
따라서 별도의 이벤트를 추가해 사용자가 기기 설정을 변경했을때를 감지하기로 했다.
const theme: Ref<Theme> = ref('light'); // 테마 값 저장할 변수
/**
* 시스템 설정 변경 감지를 위한 이벤트 리스너 설정 함수
*/
const setEvent = () => {
const darkModeMediaQuery: MediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
const handleDarkModeChange = (): void => {
theme.value = getSystemTheme()
};
darkModeMediaQuery.addEventListener('change', handleDarkModeChange); // 여기가 핵심!
};
이 코드의 핵심은 미디어 쿼리에 이벤트를 등록하는 것이다.
이렇게 되면 미디어 쿼리가 작동할때 (여기서는 테마가 변경될때) 이벤트를 받을 수 있다.
❕사용자가 테마를 변경시 기기의 설정과 별개로 동작해야 함
사용자 기기 설정과 별개로 동작하려면 별도의 클래스를 지정해서 테마를 작성해야 한다.
그러기 위해서 body에 아래처럼 테마 클래스를 지정하기로 했다.
body.theme-light
body.theme-dark
const availableThemes: Theme[] = ['light', 'dark'];
/**
* body 태그의 클래스를 업데이트하는 함수
*/
const updateBodyClass = (newTheme: Theme) => {
document.body.classList.remove(...availableThemes.map(theme => `theme-${theme}`));
document.body.classList.add(`theme-${newTheme}`);
};
// theme 값이 변경될 때마다 실행될 watch 함수
watch(
theme,
newValue => {
// 테마 값이 변경 될 경우 해당 값으로 body 클래스 업데이트
updateBodyClass(newValue);
},
{
immediate: true // 스토어가 설정될 때 즉시 실행
}
);
body에 모든 테마 클래스를 제거하고 새로 지정해주는 함수를 만들었다.
watch 함수를 이용해서 이 함수를 테마 값이 변경될때마다 실행해주면 끝이다.
❕테마 정보는 저장되어 있다가 다시 접근할때 복원되어야 함.
이걸 구현하기 위해서는 스토어가 아니라 별도의 저장 공간이 필요하다.
스토어는 새로고침시 날아가는 휘발성 스토어이기 때문에 로컬스토리지에 테마 값을 저장하기로 했다.
저장하는 기능을 별도로 만들어도 되지만 이미 잘 만들어진 피니아 플러그인이 있다.
Home | pinia-plugin-persistedstate
pinia-plugin-persistedstate Configurable persistence and rehydration of Pinia stores.
prazdevs.github.io
이 플러그인을 사용하면 로컬스토리지에 저장하는 과정을 우리가 신경쓸 필요 없다.
이 설정을 위한 코드는 너무 단순해서 여기에 기재하지 않는다.
전체 코드 혹은 라이브러리 예제를 참고하면 된다.
❕테마를 변경할때 새로고침 등의 액션 없이 즉각적으로 반영되어야 함.
이 기능을 위한 코드는 이미 위에 작성되었다.
테마 변수를 ref 변수로 선언하고 watch를 통해서 값이 변경될때마다 body에 클래스를 지정하고 있으니 해당 값을 css에서 사용하면 된다.
body.theme-light {
color-scheme: light;
background-color: #fff;
color: #333;
}
body.theme-dark {
color-scheme: dark;
background-color: #000;
color: #fff;
}
❕라이트 / 다크 모드 뿐만 아니라 그외의 테마도 지원되어야 함
사용자의 기기 설정을 유지하기 위해서는 라이트 / 다크 모드가 아니라 시스템 모드도 별개로 필요하다.
그 외에도 진짜 #000을 배경으로 사용하는 true-dark 모드를 지원하기로 했다.
이걸 위해서는 여태 작성한 코드를 약간 수정해야 한다.
// theme 상태를 Ref 객체로 생성.
const theme: Ref<Theme> = ref('system'); // 기본 값을 system으로 설정
const availableThemes: Theme[] = ['system', 'light', 'dark', 'true-dark']; // system과 true-dark 모드 추가
/**
* 시스템 설정 변경 감지를 위한 이벤트 리스너 설정 함수
*/
const setEvent = () => {
const darkModeMediaQuery: MediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
const handleDarkModeChange = (): void => {
if (theme.value === 'system') {
// 더 이상 기기 설정이 변경될때 테마 값을 변경하지 않고 body에 클래스 명만 변경한다.
// 이 기능은 system 일때만 작동하도록 한다.
// system이 아닐때 body 클래스 변경은 밑에 있는 watch에서 담당한다.
updateBodyClass(getSystemTheme());
}
};
darkModeMediaQuery.addEventListener('change', handleDarkModeChange);
};
// theme 값이 변경될 때마다 실행될 watch 함수
watch(
theme,
newValue => {
if (newValue === 'system') {
// 'system'으로 설정될 경우 현재 시스템 테마에 따라 body 클래스 업데이트
updateBodyClass(getSystemTheme());
} else {
// 'system' 외의 값으로 설정될 경우 해당 값으로 body 클래스 업데이트
updateBodyClass(newValue);
}
},
{
immediate: true // 스토어가 설정될 때 즉시 실행
}
);
setEvent 함수를 변경해서 system 일때만 body 클래스를 업데이트 하도록 변경했다.
이러면 더 이상 기기 설정 변경시 테마 값이 변경되지는 않지만 body 클래스만 변경되기 때문에 사용자는 실제로 테마가 변경되는 것처럼 보이게 된다.
사용자가 테마를 설정할때 body에 반영하는건 watch 함수가 담당한다.
👍 완성된 전체 코드
import { defineStore } from 'pinia';
import { computed, Ref, ref, watch } from 'vue';
// Theme 타입 정의: 'light', 'dark', 'true-dark', 'system' 중 하나의 값을 가질 수 있음
export type Theme = 'light' | 'dark' | 'true-dark' | 'system';
// Pinia 스토어 정의
export const useThemeStore = defineStore(
'theme', // 스토어 ID
() => {
// theme 상태를 Ref 객체로 생성.
const theme: Ref<Theme> = ref('system');
const availableThemes: Theme[] = ['system', 'light', 'dark', 'true-dark'];
/**
* 현재 시스템 설정에 기반한 테마 값을 반환하는 함수
*/
const getSystemTheme = (): Theme => {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
/**
* 시스템 설정 변경 감지를 위한 이벤트 리스너 설정 함수
*/
const setEvent = () => {
const darkModeMediaQuery: MediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
const handleDarkModeChange = (): void => {
if (theme.value === 'system') {
// 설정이 변경될때 body에 클래스 명만 변경한다.
// 이 기능은 system 일때만 작동하도록 한다.
// system이 아닐때 body 클래스 변경은 밑에 있는 watch에서 담당한다.
updateBodyClass(getSystemTheme());
}
};
darkModeMediaQuery.addEventListener('change', handleDarkModeChange);
};
/**
* body 태그의 클래스를 업데이트하는 함수
*/
const updateBodyClass = (newTheme: Theme) => {
document.body.classList.remove(...availableThemes.map(theme => `theme-${theme}`));
document.body.classList.add(`theme-${newTheme}`);
};
// theme 값이 변경될 때마다 실행될 watch 함수
watch(
theme,
newValue => {
if (newValue === 'system') {
// 'system'으로 설정될 경우 현재 시스템 테마에 따라 body 클래스 업데이트
updateBodyClass(getSystemTheme());
} else {
// 'system' 외의 값으로 설정될 경우 해당 값으로 body 클래스 업데이트
updateBodyClass(newValue);
}
},
{
immediate: true // 스토어가 설정될 때 즉시 실행
}
);
return {
theme,
availableThemes,
setEvent
};
},
{
// Pinia 스토어 지속성 설정
persist: {
key: 'theme'
}
}
);
전체 프로세스는 아래와 같다.
- 기본 테마는 시스템으로 설정된다.
- 이 테마 값은 pinia-plugin-persistedstate 라이브러리를 통해서 로컬스토리지에 저장되고, 다시 접근할때 자동으로 복원된다. 따라서 우리는 theme 값 저장/복원에 신경쓸 필요가 없다.
- watch를 통해서 (immediate 옵션이 있으므로) 처음에 updateBodyClass 함수가 호출된다. 이 함수는 body에 클래스를 지정해준다.
- 실제 테마 스타일은 body 클래스를 참조해서 css로 제공된다.
- 테마 값이 system일 경우 setEvent를 통해서 등록된 이벤트에서 updateBodyClass 함수를 호출해 body 클래스를 변경한다.
- 실제 적용되는 테마는 body에 클래스로, 값은 스토어의 ref 변수로 저장된다. 두 값은 system일 경우 서로 다르게 지정될 수 있다.
이 코드만 가지고 자동으로 동작하면 좋겠지만 피니아는 호출하지 않는 이상 동작하지 않기 때문에...
App.vue에 아래 코드를 추가했다.
// App.vue
<template>
<router-view />
</template>
<script setup lang="ts">
import { useThemeStore } from '@/app/stores/layoutStore.ts';
import { RouterView } from 'vue-router';
// service initialize
// 기본적으로 사용할 스토어 설치.
// 글로벌하게 사용할 목적
// 테마 이벤트 세팅
// 이걸 해야 시스템 테마 변경시 이벤트 감지 가능
const themeStore = useThemeStore();
themeStore.setEvent();
</script>
<style scoped lang="scss"></style>
'개발 > Frontend' 카테고리의 다른 글
[CSS] 다크모드 완벽 지원하기 (1) | 2024.01.24 |
---|---|
JavaScript로 윈도우 탐색기처럼 정렬하기 (2) | 2019.10.16 |