개발후라이
개발후라이
개발후라이
  • 분류 전체보기 (285)
    • Web Front End (74)
      • Javascript & Typescript (26)
      • React (11)
      • Vue (4)
      • Nodejs (1)
      • HTML (6)
      • CSS (7)
      • HTTP (6)
      • 책 - Review (8)
    • TIL (0)
    • Problem Solved (135)
      • 알고리즘 (4)
      • BOJ (67)
      • Programmers (8)
      • HackerRank (33)
      • LeetCode (23)
    • 회고 (4)
      • 오늘의 회고 (16)
      • 주간 회고 (15)
      • 월간 회고 (7)
      • WakaTime (9)
    • Git (3)
    • 기타 (15)
      • 취업 (5)
      • 자격증 (1)

블로그 메뉴

  • GitHub
  • LinkedIn
  • 홈

인기 글

태그

  • 개발자
  • 프론트엔드
  • JavaScript
  • 노개북
  • 자바스크립트
  • TypeScript
  • 노마드북클럽
  • 오늘의회고
  • 릿코드
  • 회고

최근 댓글

최근 글

전체 방문자
오늘
어제

티스토리

hELLO · Designed By 정상우.
개발후라이

개발후라이

[React + TS + styled-component] 다크모드 적용하기
Web Front End/React

[React + TS + styled-component] 다크모드 적용하기

2020. 8. 14. 19:06
반응형

[React + TS + styled-component] 다크모드 적용하기

 

화면 노출이 증가함에 따라 다크모드를 지원하는 운영체제와 사이트가 증가하고 있습니다.
이번 포스팅에서는 React + TypeScript + styled-component 환경에서 custom hook을 사용해 다크모드를 적용하는 방법을 얘기하려고 합니다.

  • 전체 코드: Github Link
  • 예제 데모 링크: Demo

개요

  1. 전체 구조
  2. 스타일 테마 타입 정의 및 객체 생성
  3. 테마 모드를 관리하는 custom hook 작성
  4. 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의 분기문 안에 넣습니다.
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에 현재 테마 모드를 넣어 스타일이 그에 맞게 보이도록 하고, 클릭 이벤트를 걸어 클릭하면 테마가 바뀌도록 합니다.

Screen Shot 2020-08-13 at 16 01 13Screen Shot 2020-08-13 at 16 07 11

토글 컴포넌트

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;
  • 진행 중인 토이 프로젝트에 최종 적용 모습입니다.

Screen Shot 2020-08-13 at 16 01 54Screen Shot 2020-08-13 at 16 01 40

참고 링크

  • https://css-tricks.com/a-dark-mode-toggle-with-react-and-themeprovider/
  • https://velog.io/@hwang-eunji/styled-component-typescript#2-다수-props-사용시-interface-작성
  • https://styled-components.com/docs/api#typescript
  • https://ko.reactjs.org/docs/hooks-custom.html#using-a-custom-hook
반응형
저작자표시
    'Web Front End/React' 카테고리의 다른 글
    • [React] 환경설정 제대로 알고 하기 (without CRA) ②
    • [React] Infinite Scroll과 Skeleton Loading 만들기
    • [React] 환경설정 제대로 알고 하기 (without CRA) ①
    • [React + Emotion] 리액트에 글로벌 스타일 적용하기
    개발후라이
    개발후라이
    어제보다 오늘 발전하기 위한 공간 https://github.com/choisohyun

    티스토리툴바