과거에 styled-components의 ThemeProvider로 다크모드를 구현하고 있었는데, setTheme 함수를 토글버튼에 직접 전달하다 보니 props 드릴링이 걱정되고 추상화가 잘 안되어서 코드가 지저분해 보여서 리팩토링을 하기로 했습니다.
테마의 상태를 관리하기 위해 평소에 global state 라이브러리를 많이 사용하는데 현재 개인 프로젝트에서 전역적으로 관리할 수 있는 상태는 테마뿐이라 따로 설치하지 않고 가볍게 사용할 수 있는 것을 찾아보았습니다.
패키지.
처음에는 URI 쿼리 문자열로 상태를 관리하는 방법을 생각했습니다.
그러나 사용자가 직접 URI를 수정할 수 있기 때문에 예상치 못한 변경이 발생할 수 있고, URI가 변경될 때마다 브라우저 기록이 불필요하게 누적될 수 있다는 단점이 있습니다.
그래서 다음 대안으로 고려하고 있던 컨텍스트 API를 사용하기로 했습니다.
리팩토링은 사용자 지정 후크를 사용하여 가능한 한 많이 논리를 모듈화하고 데이터를 추상화하려고 했습니다.
테마 상태 관리
const ThemeContext = createContext<ThemeContextType>({
theme: THEME_KIND.LIGHT,
setTheme: () => {},
});
export function ThemeContextProvider({
children,
}: {
children: React.ReactNode;
}) {
const (theme, setTheme) = useState<ThemeKind>(THEME_KIND.LIGHT);
const value = useMemo(() => ({ theme, setTheme }), (theme));
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
export const useThemeContext = () => useContext(ThemeContext);
테마 상태는 공급자에 배치되고 최상위 수준에서 관리됩니다.
가볍게 사용하려고 Context API를 선택했는데 오래 사용하다보니 로직을 다시 이해하는데 꽤 시간이 걸렸네요…
하나의 프로바이더만 사용하기 위해 값을 객체 형태로 만들었다.
결과적으로 개체의 주소 값이 렌더링될 때마다 변경되면 하위 구성 요소의 다시 렌더링도 발생할 수 있으므로 useMemo로 메모했습니다.
사실 컨텍스트에 의해 관리되는 상태는 하나뿐인데 왜 귀찮게 할까요? 싶지만 나중에 스테이터스가 오를 수 있기 때문에 이런 경우 미리 메모해 두는 것이 좋을 것 같습니다.
useModeTheme 사용자 정의 후크
function useModeTheme() {
const { theme, setTheme } = useThemeContext();
const { getItem: getStoredTheme, setItem: setStoredTheme } =
useWebStorage<ThemeKind>({
key: "theme",
kind: STORAGE_KIND.LOCAL,
});
const handleChangeTheme = (nextTheme: ThemeKind) => {
setStoredTheme(nextTheme);
setTheme(nextTheme);
};
useEffect(() => {
const storedTheme = getStoredTheme();
if (storedTheme === THEME_KIND.DARK || storedTheme === THEME_KIND.LIGHT) {
handleChangeTheme(storedTheme);
return;
}
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
handleChangeTheme(THEME_KIND.DARK);
}
}, ());
return { theme, handleChangeTheme };
}
export default useModeTheme;
- useThemeContext에서 테마 및 setTheme을 가져옵니다.
- 지정된 테마를 로컬 저장소에 저장하려면 useWebStorage 후크를 통해 모듈화된 기능을 가져옵니다.
- handleChangeTheme 함수를 생성하여 새로운 테마 정보를 인자로 전달하면 각각 setTheme와 setStoredTheme를 통해 context와 storage에 state가 반영된다.
- useEffect를 사용하면 초기 렌더링 중에 다음 작업이 실행됩니다.
- 저장소에 저장된 상태가 있으면 가져와서 반영합니다.
- 사용자 시스템에 설정된 테마가 다크모드일 경우 반영됩니다.
- 저장소에 저장된 상태가 있으면 가져와서 반영합니다.
테마 전환 버튼
function ThemeToggle({ menuArr }: Props) {
const { theme, handleChangeTheme } = useModeTheme();
const { currentTab, highlight, handleBtnClick } = useThemeToggle({
theme,
handleChangeTheme,
});
return (
<Style.TabContainer>
<Style.Highlight left={highlight.left} width={highlight.width} />
<Style.TabMenu>
{menuArr.map((menu, index) => (
<li
key={`${index.toString()}-${menu}`}
id={`${index}`}
className={currentTab === index ? "focused" : ""}
onClick={handleBtnClick}
role="none"
>
{menu}
</li>
))}
</Style.TabMenu>
</Style.TabContainer>
);
}
export default ThemeToggle;
- useModeTheme 후크에서 테마 상태 및 handleChangeTheme 함수를 가져옵니다.
- 받은 것은 useThemeToggle hook에 전달되고 useThemeToggle은 토글에서 사용할 로직을 반환합니다.