Next.js로 나만의 블로그 만들기 [2] Recoil과 Styled-components로 다크 모드 구현하기

May 12, 2023

Recoil과 theme-provider로 만들어보는 다크 모드

Next.js로 나만의 블로그 만들기 [2] Recoil과 Styled-components로 다크 모드 구현하기

개요

요새 좀 친다는 블로그라면 죄다 다크 모드를 지원한다. 다크 모드를 지원하면 전력소모도 줄어들고 눈의 피로도 좀 더 줄일 수도 있기 때문이다. 또 워낙 개발업계 종사자들은 특히 다크 모드에 대한 강박이 존재하는 것 같기도 하다.
라이트 모드를 대하는 흔한 개발자의 모습
라이트 모드를 대하는 흔한 개발자의 모습
이러한 시대의 흐름을 따라서 다크 모드를 구현하지 않을 수가 없었다.

다크모드 구현 방법

다크모드의 구현하는데에는 여러가지 옵션이 주어진다. 라이브러리로 Chakra UITailwind를 사용 할 수도 있고 혹은 Sass같은 전처리기를 사용할 수도 있다. 하지만 이미 styled-components를 사용 중이니 이번엔 styled-componentstheme-providerRecoil을 사용해서 구현 해 볼 예정이다.

theme-provider

theme-provider란 styled-components에서 테마를 구현하기 위해 제공해주는 기능으로서 Context API를 기반으로 작동한다. theme-provider의 하위에 위치한 컴포넌트들은 prop으로 theme를 받을 수 있게 되며 해당 theme의 CSS 값을 할당 할 수 있게 된다.

theme별 CSS 작성하기

1// .../styles/themeStyles.ts 2 3export const lightTheme = { 4 bgColor: '#fff', 5 6 categoryTextColor: '#000', 7 categoryColor: '#fff', 8 categoryShadow: 'inset 5px 5px 10px #ededed,inset -5px -5px 10px #ffffff, 5px 5px 10px #0000001b', 9 10 selectedCategoryTextColor: '#fff', 11 selectedCategoryShadow: 'none', 12 selectedCategoryColor: '#000', 13 14 textColor: '#000', 15 fontWeight: 400, 16 17 tagColor: '#000', 18 tagTextColor: '#fff', 19 20 blockColor: '#f1f1f1', 21 22 togglerColor: '#e3e3e3', 23 togglerButtonColor: '#565656', 24 togglerButtonShadow: 'inset 6px 6px 5px #4b4b4b, inset -6px -6px 5px #616161', 25 togglerShadow: 'inset 6px 6px 5px #c3c3c3, inset -6px -6px 5px #fdfdfd', 26}; 27 28export const darkTheme = { 29 bgColor: '#1c1c1c', 30 31 categoryTextColor: '#fff', 32 categoryColor: '#404040', 33 categoryShadow: 'none', 34 35 selectedCategoryTextColor: '#000', 36 selectedCategoryShadow: 'inset 5px 5px 10px #ededed,inset -5px -5px 10px #ffffff, 5px 5px 10px #0000001b', 37 selectedCategoryColor: '#fff', 38 39 textColor: '#fff', 40 fontWeight: 300, 41 42 tagColor: '#fff', 43 tagTextColor: '#000', 44 45 blockColor: '#7d7d7d', 46 47 togglerColor: '#5c5c5c', 48 togglerButtonColor: '#fff', 49 togglerButtonShadow: 'inset 6px 6px 5px #d9d9d9, inset -6px -6px 5px #fffff', 50 togglerShadow: 'inset 6px 6px 5px #4e4e4e, inset -6px -6px 5px #6a6a6a', 51}; 52 53export const theme = { 54 lightTheme, 55 darkTheme, 56}; 57 58export default theme; 59
객체 형태의 themelightThemedarkTheme를 프로퍼티로 할당한 후 export한다. 이후에 컴포넌트에서 props.theme에 접근시 현재 테마에 맞는 값을 얻을 수 있게 된다.

ThemeProvider 설정하기

1// .../components/Layout.tsx 2 3type Props = { 4 children: JSX.Element, 5} 6 7const Layout = ({ children } : Props) => { 8 const [currentTheme, setCurrentTheme] = useRecoilState(currentThemeState); 9 10 useEffect(() => { 11 if (localStorage.getItem('dark_mode') !== undefined) { 12 const localTheme = Number(localStorage.getItem('dark_mode')); 13 setCurrentTheme(localTheme); 14 } 15 }, [setCurrentTheme]); 16 17 return ( 18 <ThemeProvider theme={currentTheme === ThemeFlag.dark ? darkTheme : lightTheme}> 19 <GlobalStyle> 20 <Header/> 21 <LayoutBox> 22 {children} 23 </LayoutBox> 24 <Footer /> 25 </GlobalStyle> 26 </ThemeProvider> 27 ) 28} 29 30export default Layout; 31 32 33// .../components/GlobalStyleBox.tsx 34 35import styled from 'styled-components' 36 37type Props = { 38 children: JSX.Element[], 39} 40 41const GlobalStyleBox = styled.div` 42 position: relative; 43 color: ${(props) => props.theme.textColor}; 44 font-weight: ${(props) => props.theme.fontWeight};; 45 background-color: ${(props) => props.theme.bgColor}; 46 47 transition: all 0.5s; 48 49 a { 50 color: ${(props) => props.theme.textColor}; 51 } 52` 53 54const GlobalStyle = ({ children } : Props) => { 55 return ( 56 <GlobalStyleBox> 57 {children} 58 </GlobalStyleBox> 59 ) 60} 61 62export default GlobalStyle; 63 64 65// .../store/theme.ts 66 67import { atom } from 'recoil'; 68 69export enum ThemeFlag { 70 light, 71 dark, 72} 73 74export const currentThemeState = atom({ 75 key: 'currentThemeState', 76 default: ThemeFlag.light, 77}); 78
styled-components에서 import한 ThemeProvider를 모든 컴포넌트를 감싸는 최상위 컴포넌를 감싸도록 작성한다. 이후 propstheme를 할당하면 따로 props를 부여하지 않아도 하위 컴포넌트에서 해당 테마로 접근이 가능해진다. 예를 들어서 GlobalStyleBox 컴포넌트 같은 경우 themeprops를 따로 설정해 주지 않았지만, ${(props) => props.theme.textColor}처럼 접근이 가능해진다. 참고로 props.themeThemeProvider에 할당된 theme를 기준으로 값이 정해진다. 따라서 theme는 전역 상태관리를 통해 관리해주면 좀 더 편하게 컨트롤 할 수 있게 된다. 또한 최초 접속시 로컬스토리지를 탐색하여 기존에 설정한 테마가 존재하는지 판별한다.

테마 토글 버튼 작성

1// .../components/ThemeToggle.tsx 2 3import styled from "styled-components"; 4import ThemeButton from "../atoms/ThemeButton"; 5import { useRecoilState } from 'recoil'; 6import { ThemeFlag, currentThemeState } from "@/stores/theme"; 7 8const ToggleBox = styled.div` 9 display: flex; 10 align-items: center; 11 width: 60px; 12 height: 30px; 13 border-radius: 15px; 14 background-color: ${(props) => props.theme.togglerColor}; 15 box-shadow: ${(props) => props.theme.togglerShadow}; 16 17 -webkit-user-select:none; 18 -moz-user-select:none; 19 -ms-user-select:none; 20 user-select:none; 21 22 .icons { 23 display: flex; 24 justify-content: space-between; 25 width: 100%; 26 padding: 0 6px; 27 } 28 29 .material-symbols-outlined { 30 color: ${(props) => props.theme.textColor};; 31 font-size: 20px; 32 font-variation-settings: 33 'FILL' 1, 34 'wght' 400, 35 'GRAD' 0, 36 'opsz' 40 37 } 38 39 &:hover { 40 cursor: pointer; 41 } 42` 43 44const ThemeToggle = () => { 45 const [currentTheme, setCurrentTheme] = useRecoilState(currentThemeState); 46 47 function changeThemeHandler() { 48 if (currentTheme === ThemeFlag.dark) { 49 setCurrentTheme(ThemeFlag.light); 50 localStorage.setItem('dark_mode', String(ThemeFlag.light)); 51 } 52 else { 53 setCurrentTheme(ThemeFlag.dark); 54 localStorage.setItem('dark_mode', String(ThemeFlag.dark)); 55 } 56 } 57 58 return ( 59 <ToggleBox onClick={changeThemeHandler}> 60 <div className='icons'> 61 <span className='material-symbols-outlined'> 62 clear_night 63 </span> 64 <span className='material-symbols-outlined'> 65 clear_day 66 </span> 67 </div> 68 <ThemeButton currentTheme={currentTheme}/> 69 </ToggleBox> 70 ) 71} 72 73export default ThemeToggle; 74 75 76// .../components/ThemeButton.tsx 77 78import React from "react"; 79import styled from "styled-components"; 80import { ThemeFlag } from "@/stores/theme"; 81 82type Props = { 83 currentTheme: ThemeFlag; 84} 85 86type ThemeButtonProps = { 87 currentTheme: number; 88} 89 90const ThemeButtonSwitch = styled.button<ThemeButtonProps>` 91 width: 30px; 92 height: 30px; 93 position: absolute; 94 border: none; 95 border-radius: 50%; 96 background-color: ${(props) => props.theme.togglerButtonColor}; 97 box-shadow: ${(props) => props.theme.togglerButtonShadow}; 98 99 transform: ${(props) => props.currentTheme === ThemeFlag.dark ? 'translateX(30px)' : 'translateX(0)'}; 100 transition: all 0.3s; 101 102 &:hover { 103 cursor: pointer; 104 } 105` 106 107const ThemeButton = ({currentTheme} : Props) => { 108 return ( 109 <ThemeButtonSwitch currentTheme={currentTheme}> 110 </ThemeButtonSwitch> 111 ) 112} 113 114export default ThemeButton; 115
테마를 컨트롤하는 토글러 컴포넌트를 작성한 뒤 클릭마다 전역 상태의 테마를 변경하도록 구현한다. 이후 로컬스토리지에도 저장하여 브라우저의 기본 테마 세팅을 변경한다.

결과

결과물
결과물