🔥
React + Styled-Components
를 사용하여 Light / Dark 모드를 구현해 보자.
npm install --save styled-components
모든 색상들을 가지고 있는 object 이다.
Typescript를 사용한다면, styled.d.ts
파일을 생성해 준뒤, type을 지정해줘야 한다.
기본적으로 DefaultTheme 인터페이스는 비어 있으므로 확장해야 한다.
//styled.d.ts
import "styled-components";
declare module "styled-components" {
export interface DefaultTheme {
textColor: string;
bgColor: string;
borderColor: string;
}
}
theme.ts
파일을 생성해 준뒤, 사용 할 색상을 넣어준다.
//theme.ts
import { DefaultTheme } from "styled-components";
export const darkTheme: DefaultTheme = {
bgColor: "#2F2F2F",
textColor: "#e5e5e5",
borderColor: "#ffffff",
};
export const lightTheme: DefaultTheme = {
bgColor: "#ffffff",
textColor: "#000000",
borderColor: "#1e1e1e",
};
그리고 전역스타일 처리하는 함수( createGlobalStyle
) 를 App.tsx
에 만들어준다.
const GlobalStyle = createGlobalStyle`
body {
color:${(props) => props.theme.textColor};
background-color: ${(props) => props.theme.bgColor};
}
`;
그리고 난뒤에 theme이라는 props를 받기위해 App을 themeProvider
로 감싸주면된다. 그렇게 되면 App은 theme에 접근할 수 있다.
//App.tsx
import { createGlobalStyle, ThemeProvider } from "styled-components";
import { darkTheme, lightTheme } from "./theme";
import { RouterProvider } from "react-router-dom";
import router from "./Router";
const GlobalStyle = createGlobalStyle`
body {
color:${(props) => props.theme.textColor};
background-color: ${(props) => props.theme.bgColor};
}
`;
function App() {
return (
<ThemeProvider theme={darkTheme}> //or theme={lightTheme}
<GlobalStyle />
<RouterProvider router={router} />
</ThemeProvider>
);
}
export default App;
위와 같이 theme을 darkTheme로 설정하면 darkTheme에 지정한 색상으로 설정이 되고, lightTheme로 설정하면 lightTheme에 지정한 색상으로 설정이 된다.
Light Mode / Dark Mode 를 사용자가 변경할 수 있도록 버튼을 생성해 보자.
전역상태관리를 위해 Recoil 을 사용하였고, 사용할 Icon은 Fontawesome 을 사용하였다.✔︎ 설치
npm install recoilnpm i --save @fortawesome/react-fontawesome@latest
npm i --save @fortawesome/free-solid-svg-icons
npm i --save @fortawesome/free-regular-svg-icons
Recoil에 관한 자세한 내용은 따로 정리할 예정이다.
지금은 DarkMode구현에 필요한 설정정도만 보도록하자.
Recoil 상태를 사용하는 컴포넌트들은 atom context를 가지는 RecoilRoot
가 필요하다. 해당 Root 이하의 컴포넌트는 모두 같은 전역 상태의 값을 사용할 수 있다.
//index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "react-query";
import App from "./App";
import { RecoilRoot } from "recoil";
const client = new QueryClient();
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<RecoilRoot>
<QueryClientProvider client={client}>
<App />
</QueryClientProvider>
</RecoilRoot>
</React.StrictMode>
);
이제 값을 저장할 atom을 만들어준다. atoms.ts파일을 생성한뒤 파일 안에 atom을 만들어 준다. atom은 유니크한 key와 default 값
두가지가 필요하다.
//atoms.ts
import { atom } from "recoil";
export const isDarkAtom = atom({
key: "Dark", //
default: false, //기본값
});
isDarkAtom를 읽고 항목 텍스트를 업데이트하고, 완료된 것으로 표시하고, 삭제하는 데 사용하는 setter 함수를 얻기 위해 useRecoilState()
를 사용한다.
//mode.tsx
import { faMoon, faSun } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "react-router-dom";
import { useRecoilState } from "recoil";
import styled from "styled-components";
import { isDarkAtom } from "../../atoms";
import { Tab } from "./Header";
function Mode() {
const [darkAtom, setDarkAtom] = useRecoilState(isDarkAtom);
const toggleMode = () => {
setDarkAtom((prev) => !prev);
};
return (
<ModeContainer>
{!darkAtom ? (
<Icon onClick={toggleMode}>
<FontAwesomeIcon icon={faSun} />
</Icon>
) : (
<Icon onClick={toggleMode}>
<FontAwesomeIcon icon={faMoon} />
</Icon>
)}
<Tab>
<Link to={"/login"}>Login</Link>
</Tab>
</ModeContainer>
);
}
export default Mode;
이제 다시 App.tsx로 와서 isDarkAtom값을 불러와 ThemeProvider에게 넣어주도록 하자. 이 atom의 항목을 읽기만 한다면 useRecoilValue()
훅을 사용할 수 있다.
//App.tsx
import { createGlobalStyle, ThemeProvider } from "styled-components";
import { darkTheme, lightTheme } from "./theme";
import { useRecoilValue } from "recoil";
import { isDarkAtom } from "./atoms";
import { RouterProvider } from "react-router-dom";
import router from "./Router";
const GlobalStyle = createGlobalStyle`
body {
color:${(props) => props.theme.textColor};
background-color: ${(props) => props.theme.bgColor};
transition: background-color 0.5s, color 0.5s;
}
`;
function App() {
const isDark = useRecoilValue(isDarkAtom);
return (
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<GlobalStyle />
<RouterProvider router={router} />
</ThemeProvider>
);
}
export default App;
설정한 값을 새로고침 해도 유지할 수 있도록, localStorage에 상태를 저장하도록 해보자.
//mode.tsx
import { faMoon, faSun } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "react-router-dom";
import { useRecoilState } from "recoil";
import styled from "styled-components";
import { isDarkAtom } from "../../atoms";
import { Tab } from "./Header";
function Mode() {
const [darkAtom, setDarkAtom] = useRecoilState(isDarkAtom);
const toggleMode = () => {
setDarkAtom((prev) => !prev);
localStorage.setItem("mode", String(!darkAtom)); //localStorage에 값 저장.
};
return (
<ModeContainer>
{!darkAtom ? (
<Icon onClick={toggleMode}>
<FontAwesomeIcon icon={faSun} />
</Icon>
) : (
<Icon onClick={toggleMode}>
<FontAwesomeIcon icon={faMoon} />
</Icon>
)}
<Tab>
<Link to={"/login"}>Login</Link>
</Tab>
</ModeContainer>
);
}
export default Mode;
그리고 atom의 defalt값도 바꾸어준다.
//atoms.ts
import { atom } from "recoil";
export const isDarkAtom = atom({
key: "Dark", //
default: localStorage.getItem("mode") === "true" ? true : false,
});
이렇게 해주게 되면, 페이지에 나갔다 들어와도 사용자가 선택한 모드로 설정이 된다.
😱 하지만 위에서 보이는 것과 같이 DarkMode에서 새로고침을 하면 잠깐 LightMode가 되었다가 DarkMode가 되는 현상이 일어났다. GlobalStyle에
transition: background-color 0.5s, color 0.5s
을 지우게 되면 깜빡이는 현상은 사라지지만, 변환되는 과정이 마음에 들지 않아서 방법을 찾아 보았다.
해당부분을 개선하기 위해서는 화면이 완전히 렌더링 되기 전에 배경색을 먼저 넣어 주어야 한다.
먼저 브라우저 렌더링 순서에 대해 알아보자.
앞서 렌더링의 2번째 과정에서 script 태그가 해석되는 동안에는 HTML 파싱이 중단된다. 이러한 현상을 HTML Blocking
이라고 한다.
<!-- public/index.html -->
<head>
<script>
alert("HTML Blocking")
</script>
<title>React App</title>
</head>
이 부분에서 DOM트리가 생성되기 직전에 어떤 테마로 화면을 그려낼지 결정 하면 된다.
아래 코드를 public/index.html
에 적용 시켜 주었다.
<script type="text/javascript">
(function() {
function getInitialColorMode() {
const isClient = typeof window !== 'undefined';
if (isClient) {
const persistedColorPreference = window.localStorage.getItem('mode');
if (persistedColorPreference) {
return persistedColorPreference;
}
const systemPreference = window.matchMedia('(prefers-color-scheme: dark)');
if (systemPreference.matches) {
return 'true';
}
return 'false';
}
}
const colorMode = getInitialColorMode();
document.documentElement.style.setProperty(
'background-color',
colorMode === 'false' ? '#FFFFFF' : '#2F2F2F'
);
document.documentElement.style.setProperty(
'color',
colorMode === 'false' ? '#000000' : '#e5e5e5'
);
})();
</script>
이제 화면을 그리기 전, 위의 script를 만나 html에 배경색을 미리 넣어주어 깜빡이는 현상을 해결할 수 있다.