[React + TS + styled-component] 다크모드 적용하기
화면 노출이 증가함에 따라 다크모드를 지원하는 운영체제와 사이트가 증가하고 있습니다.
이번 포스팅에서는React + TypeScript + styled-component
환경에서custom hook
을 사용해 다크모드를 적용하는 방법을 얘기하려고 합니다.
- 전체 코드: Github Link
- 예제 데모 링크: Demo
개요
- 전체 구조
- 스타일 테마 타입 정의 및 객체 생성
- 테마 모드를 관리하는 custom hook 작성
- custom hook을 사용하여 토글 버튼 생성
1. 전체 구조
이번 포스팅에서 주로 사용하는 코드의 구조는 아래와 같습니다.
전체 환경설정은 Github 링크에서 확인하실 수 있습니다.
.
├── src
│ ├── App.tsx
│ ├── index.tsx
│ ├── views
│ │ └── MainPage
│ ├── style // 2. **스타일 테마** 타입 정의 및 객체 생성
│ │ ├── style.d.ts
│ │ └── theme.ts
│ ├── hooks // 3. 테마 모드를 관리하는 **custom hook** 작성
│ │ └── useDarkMode.ts
│ ├── components // 4. custom hook을 사용하여 **토글** 버튼 생성
│ │ └── Toggle
2. 스타일 테마 정의 및 객체 생성
- 스타일 테마를 적용하게 되면 스타일 코드를 작성할 때 theme를 props로 받아서 쓰기 때문에 하드코딩이 줄고 수정이 용이하게 됩니다.
styled-component에 테마 타입 정의
styled-component에서 테마를 커스텀하여 사용하기 위해서는 기존 타입 정의를 불러와 사용할 타입을 추가적으로 정의해야 합니다.
- v4.1.4 이상만 가능합니다.
style.d.ts
파일을 생성해 아래와 같은 구조로 타입을 선언합니다.
-
mode
: 다크모드/라이트모드로 변경될 때마다 다른 색상이 들어가게 됩니다.mode 안에서 key값을 색상 이름이 아닌 mainBackgound와 같이 쓰이는 부분의 의미로 작성하게 되면 색상값만 바뀌면 되기 때문에 편리합니다.
-
fontSize
: 같은 폰트 크기가 여러 번 중복해서 나오기 때문에 이를 줄이기 위해 xsm부터 xxl까지 폰트 크기를 지정해 줍니다. -
fontWeight
: 폰트 굵기를 많이 쓰이는 값들로 regular부터 extraBold로 지정합니다. -
이 외에 자주 쓰이는 스타일 값이 있다면 자유롭게 추가할 수 있습니다.
import "styled-components";
declare module "styled-components" {
export interface DefaultTheme {
mode: {
mainBackground: string;
primaryText: string;
secondaryText: string;
disable: string;
border: string;
divider: string;
background: string;
tableHeader: string;
themeIcon: string;
blue1: string;
blue2: string;
blue3: string;
green: string;
gray: string;
};
fontSizes: {
xsm: string;
sm: string;
md: string;
lg: string;
xl: string;
xxl: string;
};
fontWeights: {
extraBold: number;
bold: number;
semiBold: number;
regular: number;
};
}
}
테마 객체 생성
theme.ts
파일에 style.d.ts에서 정의한 mode(dark, light), fontSizes, fontWeights
객체를 적절한 값으로 생성합니다.
- theme를 사용할 때 style.d.ts에 정의한 타입과 theme 타입, 요소가 다르면 에러가 발생하니 주의해야 합니다.
- 여기서 객체를 따로 만들어 준 이유는
App.tsx
에서 테마에 맞게 객체를 구성하기 위해서 입니다.
const dark = {
mainBackground: "#292B2E",
primaryText: "#fff",
secondaryText: "rgba(255,255,255,0.45)",
disable: "rgba(255,255,255,0.25)",
border: "#d1d5da",
divider: "rgba(255, 255, 255, 0.6)",
background: "rgb(217, 223, 226)",
tableHeader: "rgba(255,255,255,0.02)",
themeIcon: "#FBE302",
// point-color
blue1: "#f1f8ff",
blue2: "#c0d3eb",
blue3: "#00adb5",
green: "#1fab89",
gray: "#393e46",
};
const light = {
mainBackground: "#fff",
primaryText: "#292B2E",
secondaryText: "rgba(0, 0, 0, 0.45)",
disable: "rgba(0, 0, 0, 0.25)",
border: "#d1d5da",
divider: "rgba(106, 115, 125, 0.3)",
background: "rgb(217, 223, 226)",
tableHeader: "rgba(0, 0, 0, 0.02)",
themeIcon: "#1fab89",
blue1: "#f1f8ff",
blue2: "#c0d3eb",
blue3: "#00adb5",
green: "#1fab89",
gray: "#393e46",
};
const fontSizes = {
xsm: "10px",
sm: "12px",
md: "16px",
lg: "20px",
xl: "24px",
xxl: "28px",
};
const fontWeights = {
extraBold: 800,
bold: 700,
semiBold: 600,
regular: 400,
};
export { dark, light, fontSizes, fontWeights };
3. 테마 모드를 관리하는 custom hook 작성
테마를 바꾸기 위해서는 테마 모드에 대한
상태 관리
가 필요합니다.
useState와 useEffect만 사용할 수도 있지만 여기에서는custom hook
을 사용해 App.tsx를 간단하게 구성하도록 하겠습니다.
custom hook
- React의 함수형 컴포넌트에서는 컴포넌트 로직을
재사용 가능한 함수
인 custom hook을 직접 만들어 사용할 수 있습니다. - 함수명에
use
키워드를 붙이면 React가 자동으로 hook으로 인식하여 사용할 수 있습니다.
useDarkMode
-
useDarkMode의 역할은
현재 테마
와테마를 변경하고 localStorage에 저장해 주는 함수
를 반환해 주어 바로 쓸 수 있게 하는 것입니다.두 개의 반환하는 값을 배열로 하기 위해 아래와 같이 타입을 선언합니다.
export const useDarkMode = (): [string, () => void] => {};
-
useState
를 활용해 theme의 초기값을 light로 설정합니다. useState에서 theme는 테마 값을 저장합니다.useState에서 setTheme를 통해 테마를 바꿔 줄 수 있습니다.
-
토글을 클릭할 때마다 테마가 변경되어야 하므로 현재 테마에 따라서 반대로 테마를 바꿔 주는 분기 처리를 진행합니다.
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
if (theme === "light") {
setTheme("dark");
} else {
setTheme("light");
}
};
- 테마를 변경하면서 사용자가 지정한 테마로 유지시키기 위해
로컬 스토리지
에 테마도 바뀔 수 있도록 지정해야 합니다. window.localStorage
키워드를 통해 로컬스토리지에 접근할 수 있습니다.- setItem으로 key, value 값을 아래와 같이 지정하여
toggleTheme
의 분기문 안에 넣습니다.
- setItem으로 key, value 값을 아래와 같이 지정하여
window.localStorage.setItem("theme", "light");
- 여기서 초기값을 localStorage에 저장된 값으로 바꿔 주면 새로고침하거나 창을 닫게 되도 사용자가 지정한 테마 모드로 저장할 수 있습니다.
const localTheme = window.localStorage.getItem("theme");
const initialState = localTheme ? localTheme : "light";
const [theme, setTheme] = useState(initialState);
4. custom hook을 사용하여 토글 버튼 생성
토글 컴포넌트 생성
- 아래와 같이 라이트/다크 모드가 변경되는 토글 버튼을 생성합니다.
- Wrapper에
현재 테마 모드
를 넣어 스타일이 그에 맞게 보이도록 하고,클릭 이벤트
를 걸어 클릭하면 테마가 바뀌도록 합니다.
토글 컴포넌트
interface IToggle {
themeMode: string;
toggleTheme: () => void;
}
const Toggle = ({ themeMode, toggleTheme }: IToggle) => {
return (
<>
<Wrapper onClick={toggleTheme} themeMode={themeMode}>
<WbSunnyIcon />
<NightsStayIcon />
</Wrapper>
</>
);
};
스타일 적용
- 스타일은
transform
을 적용하여 위치가 클릭할 때마다 위치가 이동되도록 하고, 영역 밖의 아이콘은overflow: hidden
을 통해 보이지 않도록 합니다. position: fixed
로 설정하여 우측 하단에 고정되도록 설정합니다.
interface IWrapper {
themeMode: string;
}
const Wrapper = styled.button<IWrapper>`
background: ${({ theme }) => theme.mode.mainBackground};
border: 1px solid ${({ theme }) => theme.mode.border};
box-shadow: 0 1px 3px ${({ theme }) => theme.mode.divider};
border-radius: 30px;
cursor: pointer;
display: flex;
font-size: 0.5rem;
justify-content: space-between;
align-items: center;
margin: 0 auto;
overflow: hidden;
padding: 0.5rem;
position: fixed;
z-index: 1;
width: 4rem;
height: 2rem;
bottom: 2rem;
right: 1rem;
svg {
color: ${({ theme }) => theme.mode.themeIcon};
&:first-child {
transform: ${({ themeMode }) =>
themeMode === "light" ? "translateY(0)" : "translateY(2rem)"};
transition: background 0.25s ease 2s;
}
&:nth-child(2) {
transform: ${({ themeMode }) =>
themeMode === "dark" ? "translateY(0)" : "translateY(-2rem)"};
transition: background 0.25s ease 2s;
}
}
`;
App.tsx에 토글 버튼 적용
-
위에서 만든 토글 버튼을 불러와 App.tsx에 적용시킵니다.
App은 컴포넌트 중 최상위에 속하기 때문에 모든 페이지에 보이게 하기 위해서 입니다.
-
Background
라는 스타일 컴포넌트를 만들어 테마의 백그라운드 속성을 적용하면 배경 전체 색을 변경할 수 있습니다.
// import 부분 생략
const App = () => {
const [themeMode, toggleTheme] = useDarkMode();
const theme =
themeMode === "light"
? { mode: light, fontSizes, fontWeights }
: { mode: dark, fontSizes, fontWeights };
return (
<>
<ThemeProvider theme={theme}>
<Reset />
<Toggle themeMode={themeMode} toggleTheme={toggleTheme} />
<Backgound>// 라우터 부분 생략</Backgound>
</ThemeProvider>
</>
);
};
const Backgound = styled.div`
background-color: ${({ theme }) => theme.mode.mainBackground};
`;
export default App;
- 진행 중인 토이 프로젝트에 최종 적용 모습입니다.