JadeCode

[styled-components] 타입스크립트 적용, 다크모드 본문

개발

[styled-components] 타입스크립트 적용, 다크모드

z-zero 2023. 3. 14. 12:00

최근 스타일도 컴포넌트화 하는 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의 구조를 바꾸는 등 다양하게 사용할 수 있다.

 

초기 부터 잘 세팅해 놓으면 개발을 할 때 편리하다. 스타일 가이드를 더 잘 따를 수 있게 될 것이다.

팀원들 간에 일관성을 유지하고 코드 가독성을 향상시킬 수 있도록 세팅을 하는 것이 좋다고 생각한다.

Comments