[styled-components] 타입스크립트 적용, 다크모드
최근 스타일도 컴포넌트화 하는 CSS-in-JS가 점점 뜨고있다.
직접 사용해보니 props를 넘겨 사용할 수도 있고 여러 면에서 편리하여 애착이 가는 라이브러리이다.
프로젝트를 처음 세팅할 때 styled-components 에 초기 세팅하는 법을 정리해보자.
설치
일단 프로젝트를 생성한다. 최근 공부하고 있는 next.js와 typescript로 생성하자.
npx create-next-app@latest --typescript
그리고 만든 프로젝트 내부에 styled-components를 설치한다.
npm i styled-components
npm i @types/styled-components //타입지정
타입스크립트도 사용할 예정이기 때문에 npm i @types/~ 도 설치해야된다.
또한, styled-component는 클래스명이 해쉬되어 만들어지기 때문에 디버깅하기 쉽지 않다. 따라서 babel-plugin-styled-components를 사용해야한다. babel-plugin-styled-components는 해당 스타일 컴포넌트의 이름을 해쉬 앞에 접두사를 붙여서 넣어주어 편하게 디버그할수 있다.
npm i babel-plugin-styled-components
그리고 babel 설정 파일에 추가해야한다.
프로젝트 폴더 제일 상단에 .babelrc 파일을 만들고 아래에 코드를 넣는다.
아래에 사이트를 참고하여 세부 조건도 추가한다.
//.babelrc
{
"presets": ["next/babel"],
"plugins": [
[
"babel-plugin-styled-components",
{ "fileName": true, // 코드가 포함된 파일 클래스명에 알려줌
"displayName": true, // 해당 스타일 정보 추가
"pure": true,// 사용하지 않는 속성 제거
"ssr": true// server side rendering
}
]
]
}
https://styled-components.com/docs/tooling#usage
styled-components: Tooling
Additional Tools for styled-components, babel and TypeScript plugins, testing
styled-components.com
다크모드
다크모드를 사용하려면 일단 styled.d.ts라는 선언 파일을 생성해주어야한다.
선언파일이기 때문에 types폴더 아래에 만들 것이다.
// types/styled.d.ts
import "styled-components";
declare module "styled-components" {
export interface DefaultTheme {
breakpoints: {
small: string;
medium: string;
large: string;
};
color: {
text: string;
background: string;
main: string;
sub: string;
point: string;
};
}
}
color의 경우 CSS var()을 사용할 수 있지만 다크모드를 위해 ThemeProvider를 사용할 것이다.
styles폴더 안에 theme.ts파일을 생성하여 테마 객체를 정의한다.
lightTheme과 darkTheme의 breakpoints와 다른 색깔들은 겹치는 부분이 많기 때문에 spread 연산자를 사용한다.
// styles/theme.ts
import { DefaultTheme } from "styled-components";
export const lightTheme: DefaultTheme = {
breakpoints: {
small: "768px",
medium: "992px",
large: "1200px",
},
color: {
text: "#2b2b2b",
background: "#fefefe",
main: "#f9ca24",
sub: "#f0932b",
point: "#eb4d4b",
},
};
export const darkTheme: DefaultTheme = {
...lightTheme,
color: {
...lightTheme.color,
text: "#fefefe",
background: "#2b2b2b",
},
};
Custom Hook 만들기
Next.js의 초기 세팅된 코드의 css를 보니
@media (prefers-color-scheme: dark)
란 코드와 함께 다크모드처럼 보이는 코드가 보였다. 그래서 찾아보니 시스템 환경의 테마를 인식해서 모드를 설정하는 것이다.
그래서 나도 서비스를 만들 때 사용자 설정 조건에 맞춰서 다크모드를 설정하려 custom hook을 만들 것이다.
// hooks/useTheme.ts
import { useCallback, useLayoutEffect, useState } from "react";
function useTheme() {
const [theme, setTheme] = useState<"light" | "dark">("light");
const onChangeTheme = useCallback(() => {
setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
}, []);
// 사용자가 시스템 설정으로 다크모드를 사용하고 있다면
useLayoutEffect(() => {
if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
setTheme("dark");
} else {
setTheme("light");
}
}, []);
return {
theme,
onChangeTheme,
};
}
export default useTheme;
useEffect 대신 useLayoutEffect를 사용한 이유는 브라우저가 화면을 그리기 전 동기적으로 실행되는 hook이며, 컴포넌트가 화면에 그려지기 직전에 실행되기 때문에 사용하였다. 화면에 바로 반영되어야 하기 때문이다.
ThemeProvider설정
createContext함수를 사용하여 테마 컨텍스트 생성하여 전역으로 테마를 관리하도록 한다.
// pages/_app.tsx
import type { AppProps } from "next/app";
import useTheme from "@/hooks/useTheme";
import React from "react";
import { lightTheme, darkTheme } from "@/styles/theme";
import { ThemeProvider } from "styled-components";
const defaultValue = {
// ThemeContext의 기본 값
theme: "light",
onChangeTheme: () => {},
};
export const CustomThemeContext = React.createContext(defaultValue);
export default function App({ Component, pageProps }: AppProps) {
const themeProps = useTheme();
return (
<CustomThemeContext.Provider value={themeProps}>
<ThemeProvider
theme={themeProps.theme === "light" ? lightTheme : darkTheme}
>
<Component {...pageProps} />
</ThemeProvider>
</CustomThemeContext.Provider>
);
}
테마 변경 버튼
ThemeToggle Component만들어 직접 테마 변경하도록 하기
import React, { useContext } from "react";
import styled from "styled-components";
import { CustomThemeContext } from "@/pages/_app";
function ThemeToggle() {
const { theme, onChangeTheme } = useContext(CustomThemeContext);
// 커스텀 한 테마 컨텍스트를 불러와서 전역변수로 사용하기
return (
<ToggleWrapper onClick={onChangeTheme}>
{theme === "light" ? "🌞" : "🌕"}
</ToggleWrapper>
);
}
export default ThemeToggle;
const ToggleWrapper = styled.button`
background-color: ${({ theme }) => theme.color.background};
`;
테마 변경 버튼 적용
//pages/index.tsx
import ThemeToggle from "../components/ThemeToggle";
import styled from "styled-components";
export default function Home() {
return (
<HomeWrapper>
<ThemeToggle />
<span>Home</span>
</HomeWrapper>
);
}
const HomeWrapper = styled.div`
padding: 0;
margin: 0;
height: 100vh;
background-color: ${({ theme }) => theme.color.background};
color: ${({ theme }) => theme.color.text};
`;
버튼이 생성될 때 _app.tsx에 있는 theme state를 props로 계속해서 넘겨주지 않고 Context를 사용해서 전역으로 관리하니 더 편리하다.
반응형
theme 설정에서 breakpoints 객체를 만든 이유도 ThemeProvider로 사용하기 위해서이다.
const GridWrapper = styled.div`
display: grid;
grid-template-columns: repeat(5, 1fr);
@media screen and (max-width: ${(props) => props.theme.breakpoints.medium}) {
grid-template-columns: repeat(3, 1fr);
}
@media screen and (max-width: ${(props) => props.theme.breakpoints.small}) {
grid-template-columns: 1fr;
}
`;
이런식으로 화면 크기 별로 grid의 구조를 바꾸는 등 다양하게 사용할 수 있다.
초기 부터 잘 세팅해 놓으면 개발을 할 때 편리하다. 스타일 가이드를 더 잘 따를 수 있게 될 것이다.
팀원들 간에 일관성을 유지하고 코드 가독성을 향상시킬 수 있도록 세팅을 하는 것이 좋다고 생각한다.