유저로서 페이지 기능에서 젤 신기했던게 다크모드 였다~🤹🏻
나도 개발자 되면 간지나게 이런거 만들고 싶었다 그래서 만들었따!
(클론한 밀리의 서재에는 다크모드가 없다는게 함정 ㅋㅋㅋ)
전역에서 다크모드를 사용할 수 있도록 라우터 페이지에서 useContext를 이용해서 ThmeContext를 만들었습니다
초기값으로 "light"를 넣어주고 버튼으로 "light"<=>"dark"로 토글이 되게 만들기 위해
토글함수 tottleTheme
를 만들었습니다
//Router.js
export const ThemeContext=createContext("light")
function Router(){
const [theme,setTheme]=useState("light")
const toggleTheme=()=>{
setTheme(cur=>(cur =="light"? "dark":"light"))
}
return(
<ThemeContext.Provider value={{toggleTheme,theme}}>
<div id={theme}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
{/*생략*/}
<Route path="/bookDetail" element={<BookDetail />} />
</Routes>
</div>
</ThemeContext.Provider>
)
}
다음으로는 전역으로 사용하는 common.scss에 각각의 모드에서 어떻게 동작할지 넣어줘야한다
이때 핵심은 css 적용우선순위를 이용해서 다크모드 시 모든 css를 이기고(?) 다크모드의 scss가 보여지도록 한다
<div id={theme}><Routes>...</Routes></div>
css 적용우선순위의 상위 포식자인 id
를 이용해서 상태값인 theme이==light모드일때와 theme이==dark모드일때를 작성해준다
/*common.scss*/
#light{
background-color:white;
color:black;
}
#dark{
background-color:black;
color:white;
}
근데 이렇게 했다고해서 모든 부분이 원하는 방식으로 전환되지 않을 수 있다
그럼 부분적으로 손수 커스텀을 해주어야한다
#dark.클래스네임이름 또는 태그이름
을 붙여 다크모드일때 어떻게 보일지 조정해준다
예를들어) header에 로그아웃 버튼이 있는데 원래 색상이 검은색이였다 따라서 다크모드일때는 반대로 흰색이 되어 버튼이 보일 수 있도록 별개로 조절이 필요하다
/*header.scss*/
#dark Button {
background-color: white;
color: black;
}
저는 헤더부분에 해모양의 이모티콘일때는 "light모드" 달모양의 이모티콘일때는 "dark모드"가 될 수 있도록 코드를 만들었습니다
Router.js에서 useContext를 통해 theme과 toggleTheme을 가져왔습니다
onClick={toggleTheme}
을 넣어 이모티콘을 누르면 토글이 동작할 수 있도록해주고
삼항연산자를 이용해서 theme=="light"일 때 해 이모티콘을 반대의 경우 달 이모티콘이 나타날 수 있도록 해줬습니다
//header.jsx
import React, { useContext, useState } from 'react';
import { ThemeContext } from '../../pages/Router.js'; //themeContext 가져옴
import { BsFillSunFill } from 'react-icons/bs'; //해 이모티콘
import { FaRegMoon } from 'react-icons/fa'; //달 이모티콘
function Header() {
const { theme } = useContext(ThemeContext); //themecontext를 통해 theme가져옴
const { toggleTheme } = useContext(ThemeContext); //themecontext를 통해 toggleTheme가져옴
return(
{/*생략*/}
<div className="head-right">
<div onClick={toggleTheme}>
{theme === 'light' ? <BsFillSunFill /> : <FaRegMoon />}
</div>
{/*생략*/}
</div>
)
}
createContext는 내부에 공유하길 원하는 데이터의 초깃값을 넣어두고 value 변수로 묶어줍니다 이 때 value는 객체이므로 리렌더링의 주범이 되므로 useMemo로 캐싱주는 것이 좋습니다 안 그러면 나중에 이 데이터를 쓰는 모든 컴포넌트가 매번 리렌더링됩니다
해결책
: useMemo,useCallback => 객체(함수)자체를 Memoization해준다(값과 콜백을 메모리 어딘가에 저장해두고 필요할 때 가져다 쓴다)
useMemo,useCallback이 헷갈려??
: useMemo는 어떠한 "값"을 메모이징하기 위한것 useCallback은 어떠한 "함수"를 메모이징하기 위한 것
해서 toggleTheme
함수로 인해 계속 렌더링되지 않도록 useCallback()
을 넣어주었습니다
앞으로는 useCallback의 디펜던시로 setTheme을 넣어 theme에 변화가 생겼을 때만 랜더링이 되도록 해주었습니다
export const ThemeContext = createContext('light');
function Router() {
const [theme, setTheme] = useState('light');
const toggleTheme = useCallback(() => {
setTheme(cur => (cur === 'light' ? 'dark' : 'light'));
}, [setTheme]);
return (
<ThemeContext.Provider value={{ toggleTheme,theme }}>
<div id={theme}>
<Routes>
<Route path="/" element={<Home />} />
{/*생략*/}
<Route path="/bookDetail" element={<BookDetail />} />
</Routes>
</div>
</ThemeContext.Provider>
);
}