다크모드 기능을 적용할 것이다.
먼저 라이트모드와 다크모드 스타일을 지정한다. 이 때 mainFont
, subFont
등 객체의 key값이 같아야 한다.
styles.theme.tsx
export const lightTheme = {
MAIN: "#6868AD",
SUB: "#dbd7ff",
BACKGROUND: "#fdfdff",
SUBBACKGROUND: "rgb(242, 240, 253)",
}
export const darkTheme : Theme = {
MAIN: "#dbd7ff",
SUB: "#6868AD",
BACKGROUND: "#202124",
SUBBACKGROUND: "#30373e",
}
그리고 타입을 지정해준다. 이 때typeof
를 쓰면, 지정한 객체(lightTheme)의 프로퍼티 타입들을 참조해 타입을 선언할 수 있다.
let s = "hello";
let n: typeof s;
export type Theme = typeof lightTheme;
페이지를 이동해도 적용된 테마가 유지될 수 있도록 theme provider
와 theme context
를 활용해 다크모드 상태관리를 진행할 것이다.
일반적으로 react에서 데이터는 부모에서 자식 방향으로 props
를 통해 전달한다. 그러나, 부모에서 자식이 아니라 증조에서 증손자로 전달해야되는 상황이라면 과정이 굉장히 번거로워진다. 이 때 context를 활용하면 단계마다 props를 넘겨주지 않아도 전역적으로 값을 공유할 수 있게 된다. 우리는 App 최상위에 ThemeContext.Provider를 추가해서 하위 컴포넌트들이 Context를 통해 테마에 접근할 수 있도록 만들 것이다.
const MyContext = React.createContext(defaultValue);
여기서 defalutvalue
는 컴포넌트가 적절한 provider
를 찾지 못했을 때 쓰이는 값이다.
//타입 지정
interface ContextProps {
theme: Theme
toggleTheme: () => void
}
//객체 생성
export const ThemeContext = createContext<ContextProps>({
theme: lightTheme,
toggleTheme: () => {
return null
},
})
ThemeContext 객체 생성을 마쳤다. 이제 Context 객체를 구독하고 있는 컴포넌트들이 Context Provider로부터 현재 값을 읽을 것이다.
<MyContext.Provider value={/* 어떤 값 */}>
provider는 context의 변화를 알리는 역할, 그리고 value prop을 받아서 하위 컴포넌트에게 전달하는 역할을 한다. 따라서 provider의 value가 바뀐다면, context를 구독하고 있는 컴포넌트들은 모두 리렌더링된다.
// _app.tsx
import type { AppProps } from 'next/app';
import React, { createContext } from 'react';
import { Global } from '@emotion/react';
import { GlobalStyle } from '@styles/global-styles';
import { lightTheme, darkTheme, Theme } from '@styles/theme';
import { useDarkMode } from '@hooks/useDarkMode';
import DarkModeToggle from '@components/Home/DarkModetoggle';
interface ContextProps {
theme: Theme
toggleTheme: () => void
}
//contextX객체 생성
export const ThemeContext = createContext<ContextProps>({
//테마와 테마를 변경하는 함수
theme: lightTheme,
toggleTheme: () => {
return null
},
})
//useDarkMode hook을 통해 theme과 toggleTheme return;
const { theme, toggleTheme } = useDarkMode();
function MyApp({ Component, pageProps }: AppProps) {
const { theme, toggleTheme } = useDarkMode()
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>8
<Global
styles={GlobalStyle(theme === lightTheme ? lightTheme : darkTheme)}
/>
<Component {...pageProps} />
<DarkModeToggle />
</ThemeContext.Provider>
)
}
export default MyApp
// useDarkMode.ts
import { useEffect, useState } from "react";
import { lightTheme, darkTheme, Theme } from "../styles/theme";
export const useDarkMode = () => {
const [theme, setTheme] = useState<Theme>(lightTheme);
const setMode = (mode: Theme) => {
mode === lightTheme
? window.localStorage.setItem("theme", "light")
: window.localStorage.setItem("theme", "dark");
setTheme(mode);
};
const toggleTheme = () => {
theme === lightTheme ? setMode(darkTheme) : setMode(lightTheme);
};
useEffect(() => {
const localTheme = window.localStorage.getItem("theme");
if (localTheme !== null) {
if (localTheme === "dark") {
setTheme(darkTheme);
} else {
setTheme(lightTheme);
}
}
}, []);
return { theme, toggleTheme };
};
현재 테마(theme
)와 테마를 변경하는 함수(toggleTheme
)를 리턴해주는 hook을 생성했다. nextJS는 서버 사이드 렌더링이기 때문에 window
, localStorage
, alert
등에 직접 접근하면 undefined
를 뱉어낸다. 그러나 렌더링이 일어난 후 실행되는 useEffect
의 성질을 이용하면 localStorage
를 사용할 수 있다. localStorage
에 기본적으로 라이트테마를 저장해두고, 토글이 클릭될 때마다 테마가 변경된다. 이후에 유저가 페이지를 이동하거나 새로고침을 했을 때에도 localStorage
를 참조해 테마를 유지한다.
import LightModeIcon from '@mui/icons-material/LightMode';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import styled from '@emotion/styled';
import React, { ReactElement, useContext } from 'react';
import { ThemeContext } from '@pages/_app';
import { lightTheme, Theme } from '@styles/theme';
import { MEDIA_QUERY_END_POINT } from '@constants/.';
interface ToggleProps {
theme: Theme;
}
export default function DarkModeToggle(): ReactElement {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<ToggleButton onClick={toggleTheme} theme={theme}>
{theme === lightTheme ? (
<>
<Emoji>
<DarkModeIcon aria-label="darkMoon" />
</Emoji>
<ModeContent>다크 모드</ModeContent>
</>
) : (
<>
<Emoji>
<LightModeIcon aria-label="lightSun" />
</Emoji>
<ModeContent>라이트 모드</ModeContent>
</>
)}
</ToggleButton>
);
}
useContext
를 통해 Provider
가 전달했던 theme과 toggleTheme을 구독한다. 그리고 button 컴포넌트의 onClick 이벤트에 테마를 변경하는 toggleTheme 함수를 연결해준다.
다크모드 적용방식은 2가지가 있다. 첫 번째는 globalstyle을 활용해 app에서 스타일을 내려주는 방법이다.
//global-style.ts
import { css } from "@emotion/react";
import { Theme } from "../styles/theme";
export const GlobalStyle = (props: Theme) =>
css`
body {
background: ${props.BACKGROUND};
color: ${props.MAIN_FONT};
}
`;
//app.ts
import type { AppProps } from "next/app"
import React, { createContext } from "react"
import { Global } from "@emotion/react"
import { GlobalStyle } from "@styles/global-styles"
import { lightTheme, darkTheme, Theme } from "@styles/theme"
import { useDarkMode } from "@hooks/useDarkMode"
import DarkModeToggle from "@components/Home/DarkModetoggle"
export const ThemeContext = createContext<ContextProps>({
theme: lightTheme,
toggleTheme: () => {
return null
},
})
function MyApp({ Component, pageProps }: AppProps) {
const { theme, toggleTheme } = useDarkMode()
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>8
<Global
styles={GlobalStyle(theme === lightTheme ? lightTheme : darkTheme)}
/>
<Component {...pageProps} />
<DarkModeToggle />
</ThemeContext.Provider>
)
}
export default MyApp
두 번째는 useContext를 활용해서 각 컴포넌트에서 context를 구독하는 방식이다. 팀 프로젝트에서는 컴포넌트와 스타일 지정한 변수가 많아서 2번째 방식을 선택했다.
import { useContext } from 'react';
import { Theme } from '@styles/theme';
import { ThemeContext } from '@pages/_app';
interface ThemeProps {
theme: Theme;
}
export const ListCard = () => {
const { theme } = useContext(ThemeContext);
return (
<Card theme={theme}/>
)
const Card = styled.article<ThemeProps>`
background: ${({ theme }) => theme.CARD_BACKGROUND};
}
`;
}