๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐Ÿ‘ฉ๐Ÿป‍๐Ÿ’ป dev

๋‹คํฌ ๋ชจ๋“œ ๊ตฌํ˜„ (feat. ์œ ์ €/os ์ œ์–ด๊ถŒ ๋„˜๊ธฐ๊ธฐ)

๐Ÿ‘ฉ๐Ÿป‍๐Ÿ’ป ์–ด๋‘์šด ๋ฐฐ๊ฒฝ์— ๋ฐ์€ ๊ธ€์”จ๋Š” ๋‹คํฌ ๋ชจ๋“œ, ๋ฐ์€ ๋ฐฐ๊ฒฝ์— ์–ด๋‘์šด ๊ธ€์”จ๋Š” ๊ธฐ๋ณธ UI (๋ผ์ดํŠธ ๋ชจ๋“œ)๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
๐ŸŒ‘ ๋‹คํฌ ๋ชจ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ˆˆ์˜ ํ”ผ๋กœ๋„๋ฅผ ๋‚ฎ์ถœ ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์—์„œ ์ข…์ข… ์ง€์›ํ•˜๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.
๐ŸŒ ๋ฐ์€ ์กฐ๋ช… ์•„๋ž˜์—์„œ ๋‹คํฌ๋ชจ๋“œ๋Š” ๊ฐ€๋…์„ฑ์ด ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ผ์ดํŠธ ๋ชจ๋“œ๋„ ์ง€์›ํ•ด ์ฃผ๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.
๐ŸŒ“ ๋‹คํฌ/๋ผ์ดํŠธ ๋ชจ๋“œ๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๊ฑฐ๋‚˜ os์˜ ๊ธฐ๋ณธ ์„ค์ •์„ ๋”ฐ๋ผ๊ฐˆ ์ˆ˜ ์žˆ๋‹ค.

๐Ÿค” ๊ทผ๋ฐ ์ด๋ ‡๊ฒŒ ์œ ์ €๊ฐ€ ์„ ํƒํ•œ ๋ชจ๋“œ๋ž‘ os์˜ ์„ค์ • ์ค‘ ์–ด๋–ค๊ฒŒ ๋” ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์•„์•ผ ํ•˜๋‚˜?


์˜ค๋Š˜์€ MDN Web Docs์—์„œ ํžŒํŠธ๋ฅผ ์–ป์–ด ๋‹คํฌ/๋ผ์ดํŠธ ๋ชจ๋“œ์— ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ถ€์—ฌํ•˜๋Š” ๋ฐฉ์‹์— ๋Œ€ํ•ด ์‚ดํŽด๋ณด์ž. 

 

Mac os์—์„œ ์‹œ์Šคํ…œ ์ปฌ๋Ÿฌ๋ชจ๋“œ ์„ค์ •

 

๊ตฌํ˜„

dark/light ๋ชจ๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ์ค€๋น„๋ฌผ๋“ค์„ ๋จผ์ € ์‚ดํŽด๋ณด์ž

 

โ˜‘ useAppTheme.ts ์ปค์Šคํ…€ ํ›…: ํ˜„์žฌ ๋ชจ๋“œ๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋Š” ์ปค์Šคํ…€ ํ›…

โ˜‘ provider/ThemeProvider.tsx : ํ…Œ๋งˆ ๋ชจ๋“œ๋ฅผ ์ „์ฒด์— ๊ฐ์‹ธ์ค„ ์ปดํฌ๋„ŒํŠธ

โ˜‘ provider/layout.tsx : ๋ชจ๋“  Provider๋ฅผ ํ•œ ๊ณณ์—  ๋ชจ์€ ์ปดํฌ๋„ŒํŠธ

โ˜‘ (user ์„ ํƒ) ๋ฒ„ํŠผ : light/ dark ๋ชจ๋“œ๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋Š” ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ, useAppTheme ํ›…์„ ํ™œ์šฉํ•ด์„œ ๋ชจ๋“œ๋ฅผ ํ† ๊ธ€ ํ•  ์ˆ˜ ์žˆ๋‹ค.

โ˜‘ (user/os ์„ ํƒ) ๋ฒ„ํŠผ : user ํ˜น์€ os์— ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ถ€์—ฌํ• ์ง€ ์„ ํƒํ•  ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ, os ์„ ํƒ ์‹œ os ๋ชจ๋“œ ๋”ฐ๋ผ๊ฐ€๊ณ  user ํด๋ฆญ ์„ ํƒ ์‹œ ์œ ์ €์˜ ์„ ํƒ์ด ์šฐ์„ ๋œ๋‹ค.

 

โ˜‘ ๋‹คํฌ/๋ผ์ดํŠธ ๋ชจ๋“œ ์„ค์ •ํ•œ ์ปดํฌ๋„ŒํŠธ

๋”๋ณด๊ธฐ

!! ์ž์„ธํ•œ scss ์ฝ”๋“œ๋Š” ์ƒ๋žตํ–ˆ์Šต๋‹ˆ๋‹ค!!

  •  @tripie/design-system/src/components/_body.tsx : ํฐํŠธ์™€ ๋ชจ๋“œ๋ฅผ ์•ฑ ์ „์ฒด์— ์ ์šฉํ•  ์ปดํฌ๋„ŒํŠธ
"use client";

import { useAppTheme } from "@tripie/hooks";
import classNames from "classnames";
import localFont from "next/font/local";
import { HTMLAttributes, ReactNode } from "react";
import "./_body.scss";

export interface BodyProps extends HTMLAttributes<HTMLElement> {
  children: ReactNode;
}

const maruBuri = localFont({
  src: [
    {
      path: "../../static/fonts/MaruBuri-Regular.woff",
      style: "normal",
    },
  ],
});

const Body = ({ children, className, ...props }: BodyProps) => {
  const { mode } = useAppTheme();

  return (
    <section
      className={classNames(className, mode, maruBuri.className)}
      {...props}
    >
      <div className="background-container">
        <div className="stars"></div>
        <div className="twinkling"></div>
      </div>
      <div className="layout-wrap">{children}</div>
    </section>
  );
};
export default Body;โ€‹
  •  @tripie/design-system/src/components/_body.scss : ํฐํŠธ์™€ ๋ชจ๋“œ๋ฅผ ์•ฑ ์ „์ฒด์— ์ ์šฉํ•  ์ปดํฌ๋„ŒํŠธ
@use "../../base/" as *;
@use "../../functions/" as *;
@use "../../generator/" as *;
@use "../../mixins/" as *;
@use "./night-sky" as *;
@use "sass:map";

@include generate-styles;

.dark {
  color: color(default, 800);
  @include night-sky;
}
  •  @tripie/design-system/src/components/_night-sky.scss : ํฐํŠธ์™€ ๋ชจ๋“œ๋ฅผ ์•ฑ ์ „์ฒด์— ์ ์šฉํ•  ์ปดํฌ๋„ŒํŠธ
@use "../../mixins/" as *;
@use "../../base/" as *;
@use "sass:map";

@mixin night-sky {
  .background-container {
    overflow-x: hidden;
    @include position(fixed);
  }

  .stars {
    background: rgb(0, 0, 0) url("../../static/images/stars.png") repeat;
    @include position(absolute);
    @include z-index(base);
  }

  @keyframes move-background {
    from {
      -webkit-transform: translate3d(0px, 0px, 0px);
    }
    to {
      -webkit-transform: translate3d(1000px, 0px, 0px);
    }
  }

  .twinkling {
    background: transparent url("../../static/images/twinkling.png") repeat;
    @include position(absolute);
    @include z-index(mask);
    animation: move-background 70s linear infinite;
  }

  img {
    @include position(absolute);
    @include z-index(fixed);
  }

  .layout-wrap {
    @include position(absolute);
    @include z-index(masked);
  }
}
  •  @tripie/design-system/src/components/_body.scss : ํฐํŠธ์™€ ๋ชจ๋“œ๋ฅผ ์•ฑ ์ „์ฒด์— ์ ์šฉํ•  ์ปดํฌ๋„ŒํŠธ
@use "../base/" as *;
@use "../functions/" as *;
@use "../mixins/" as *;

/// ๋ชจ๋“  ์Šคํƒ€์ผ ์ƒ์„ฑ
@mixin generate-styles {
  @include generate-reset;
  @include generate-normalize;

  @include generate-color-variables;

  @each $key, $value in $config-theme {
    @include generate-color-variables(
      $colors: $value,
      $selector: "[data-theme=#{$key}]:root" // dark ๋ชจ๋“œ์ผ ๊ฒฝ์šฐ [data-theme="dark"]:root ์ƒ์„ฑ
    );
  }

  @include generate-default;
  @include generate-media;
}

๋ชจ๋“œ ์ œ์–ด๊ถŒ์„ ์ค„ ๋Œ€์ƒ์— ๋”ฐ๋ผ ๊ตฌํ˜„์ด ์กฐ๊ธˆ์”ฉ ๋‹ฌ๋ผ์ง„๋‹ค.

1. os ๋ชจ๋“œ ์ œ์–ด๊ถŒ ๋ถ€์—ฌ

import { useEffect, useState } from "react";
import { useLocalStorage, useMediaQuery } from "usehooks-ts";

const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)";

const THEME_MODE = {
  DARK: "dark",
  LIGHT: "light",
} as const;

type ThemeMode = (typeof THEME_MODE)[keyof typeof THEME_MODE];

type UseAppThemeOutput = {
  mode: ThemeMode;
};

export const useAppTheme = (): UseAppThemeOutput => {
  const isDarkOS = useMediaQuery(COLOR_SCHEME_QUERY);
  const [osPrefersMode] = useState(
    isDarkOS ? THEME_MODE.DARK : THEME_MODE.LIGHT
  );

  const [themeMode, setThemeMode] = useLocalStorage<ThemeMode>(
    "app-theme",
    osPrefersMode ?? THEME_MODE.LIGHT
  );

  useEffect(() => {
    const root = document?.documentElement;
    setThemeMode(isDarkOS ? THEME_MODE.DARK : THEME_MODE.LIGHT);
    if (root) {
      root.dataset.theme = themeMode;
    }
  }, [themeMode, isDarkOS]);

  return {
    mode: themeMode,
  };
};โ€‹

 

os setting์—์„œ mode๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๋„ ๋˜‘๊ฐ™์ด ๋ฐ˜์˜๋œ๋‹ค.

os ๋ชจ๋“œ ๋”ฐ๋ผ๊ฐ€๊ธฐ

๋‹คํฌ ๋ชจ๋“œ์ผ ๊ฒฝ์šฐ html์˜ data-theme= "dark"์ด๊ณ , root ์ƒ‰์ƒ๋“ค๋„ ๋‹คํฌ theme์„ ๋”ฐ๋ผ๊ฐ„๋‹ค.

 

๋ผ์ดํŠธ ๋ชจ๋“œ์ผ ๊ฒฝ์šฐ html์˜ data-theme= "light"์ด๊ณ , root ์ƒ‰์ƒ๋“ค๋„ ๋””ํดํŠธ(๋ผ์ดํŠธ) theme์„ ๋”ฐ๋ผ๊ฐ„๋‹ค.

๐Ÿ‘ฉ๐Ÿป‍๐Ÿ’ป ์ด ๋ฐฉ์‹์€ os์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ๋ณ€๊ฒฝ์ด ๋˜๋ฏ€๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.
ํ•˜์ง€๋งŒ ์œ ์ €๊ฐ€ ๋งŒ์•ฝ ๋ชจ๋“œ๋ฅผ ๋ฒˆ๊ฒฝํ•˜๊ณ ์žํ•˜๋ฉด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ™”๋ฉด์—์„œ ๋ฒ—์–ด๋‚˜ ์„ค์ •์— ์ง์ ‘ ๋“ค์–ด๊ฐ€์„œ ๋ณ€๊ฒฝ์„ ํ•ด์•ผ ํ•œ๋‹ค๋Š” ์ ์—์„œ ๋ฒˆ๊ฑฐ๋กญ๋‹ค. ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ํ˜„์žฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๋งŒ ํŠน์ • ๋ชจ๋“œ๋กœ ํ•˜๊ณ  ๋‹ค๋ฅธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ os ๋ชจ๋“œ๋ฅผ ์œ ์ง€ํ•˜๋„๋ก ํ•˜๊ณ ํ”ˆ ๋‹ˆ์ฆˆ๊ฐ€ ์žˆ์„ ์ˆ˜๊ฐ€ ์žˆ์„ ์ˆ˜๋„ ์žˆ๋‹ค.

๊ทธ๋Ÿผ ์ด๋ฒˆ์—๋Š” ์œ ์ €์—๊ฒŒ ์ œ์–ด๊ถŒ์„ ๋„˜๊ธฐ๋Š” ๋ฐฉ์‹์„ ์‚ดํŽด๋ณด์ž.

2. ์œ ์ €์—๊ฒŒ ๋ชจ๋“œ ์„ ํƒ ์ œ์–ด๊ถŒ ๋ถ€์—ฌ

import { useEffect, useState } from "react";
import { useLocalStorage, useMediaQuery } from "usehooks-ts";

const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)";

const THEME_MODE = {
  DARK: "dark",
  LIGHT: "light",
} as const;

type ThemeMode = (typeof THEME_MODE)[keyof typeof THEME_MODE];

type UseAppThemeOutput = {
  mode: ThemeMode;
  toggle: () => void;
  setLight: () => void;
  setDark: () => void;
  setMode: (mode: ThemeMode) => void;
};

export const useAppTheme = (): UseAppThemeOutput => {
  const isDarkOS = useMediaQuery(COLOR_SCHEME_QUERY);
  const [osPrefersMode] = useState(
    isDarkOS ? THEME_MODE.DARK : THEME_MODE.LIGHT
  );

  const [themeMode, setThemeMode] = useLocalStorage<ThemeMode>(
    "app-theme",
    osPrefersMode ?? THEME_MODE.LIGHT
  );

  useEffect(() => {
    const root = document?.documentElement;

    if (root) {
      root.dataset.theme = themeMode;
    }
  }, [themeMode, isDarkOS]);

  return {
    mode: themeMode,
    toggle: () =>
      setThemeMode((previous) =>
        previous === THEME_MODE.DARK ? THEME_MODE.LIGHT : THEME_MODE.DARK
      ),
    setLight: () => setThemeMode(THEME_MODE.LIGHT),
    setDark: () => setThemeMode(THEME_MODE.DARK),
    setMode: (mode) => setThemeMode(mode),
  };
};

 

// app/page.tsx

"use client";
import ThemeButton from "../components/ThemeButton";

export default function Home() {
  return (
    <div>
      <ThemeButton.Toggle />

      <h1>this is home</h1>
    </div>
  );
}

์œ ์ €์—๊ฒŒ ๋ชจ๋“œ ์„ ํƒ ๊ถŒํ•œ ๋„˜๊ธฐ๊ธฐ, os setting์ด ๋ณ€๊ฒฝ๋˜์–ด๋„ ์•ฑ์˜ ๋ชจ๋“œ๋Š” ์œ ์ง€๋œ๋‹ค๋Š” ๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด ๋ฐฉ์‹์€ ํ˜„์žฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ชจ๋“œ๋ฅผ ์œ ์ €๊ฐ€ ์›ํ•˜๋Š” ๋Œ€๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค.
ํ•˜์ง€๋งŒ os ์„ค์ •์ด ๋ฐ˜์˜๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๋ถˆํŽธํ•œ ์ƒํ™ฉ์ด ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด ๋‹คํฌ ๋ชจ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”๋ฐ ์ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๋งŒ ๋ผ์ดํŠธ ๋ชจ๋“œ์ผ ๊ฒฝ์šฐ ๊ฐ‘์ž๊ธฐ ๋ˆˆ์ด ๋„ˆ๋ฌด ๋ถ€์‹œ๊ฑฐ๋‚˜,
์–ด๋‘์šด ๊ณณ์—์„œ ๋ณด๊ณ  ์žˆ๋Š” ์ค‘์ด๋ผ os ์„ค์ •์ด ๋ผ์ดํŠธ์ธ๋ฐ ์ž๋™์œผ๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ๋‹คํฌ ๋ชจ๋“œ๋กœ ์„ค์ •๋˜์–ด ์žˆ๋‹ค๋ฉด ๊ตณ์ด ๋ณ€๊ฒฝ์„ ํ•ด์•ผ ํ•œ๋‹ค๋Š” ์ ์ด ์š•์‹ฌ์„ ์ข€ ๋ถ€๋ฆฌ์ž๋ฉด ๋ถˆํŽธํ•˜๋‹ค.
๊ทธ๋ฆฌ๊ณ  ์ฒ˜์Œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ง„์ž…ํ–ˆ์„ ๊ฒฝ์šฐ ๋‹คํฌ๋ชจ๋“œ๋กœ ์„ค์ •ํ•ด์ค˜์•ผ ํ• ์ง€ ํ˜น์€ ๋ผ์ดํŠธ ๋ชจ๋“œ๋กœ ์„ค์ •ํ•ด์•ผ ํ• ์ง€ ์œ ์ €์˜ ์„ ํƒ์ด ์•„๋‹Œ ๊ฐœ๋ฐœ์ž ์ž„์˜๋Œ€๋กœ ์ •ํ•  ์ˆ˜๋ฐ–์— ์—†๋‹ค.

๊ทธ๋Ÿฐ ์ ์—์„œ os๋ฅผ ๋”ฐ๋ผ๊ฐ€๋Š” ๊ฑธ ๊ธฐ๋ณธ์œผ๋กœ (์œ ์ €์˜ ์ทจํ–ฅ ๊ณ ๋ ค) ํ•˜๋ฉด์„œ๋„ ์œ ์ €๊ฐ€ ์›ํ•˜๋Š” ๋Œ€๋กœ ์„ค์ •์„ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•  ์ˆ˜ ์—†์„๊นŒ?
๊ทธ๋Ÿฌ๊ธฐ ์œ„ํ•ด ๊ณต๋ถ€๋ฅผ ํ•˜๋ฉด์„œ ์ข…์ข… ๋ฐฉ๋ฌธํ–ˆ๋˜ MDN Web Docs๋Š” ๋ผ์ดํŠธ/๋‹คํฌ ๋ชจ๋“œ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋Š”์ง€ ์‚ดํŽด๋ณด์ž.

MDN์˜ ๋ผ์ดํŠธ/๋‹คํฌ ๋ชจ๋“œ

mdn์˜ ๋‹คํฌ/๋ผ์ดํŠธ ๋ชจ๋“œ

 

๊ธฐ๋ณธ์ ์œผ๋กœ os ์„ค์ •์„ ๋”ฐ๋ผ๊ฐ€๋„๋ก ํ•˜๊ณ , light์ด๋‚˜ dark ๋ชจ๋“œ๋ฅผ ์„ ํƒํ•˜๋ฉด os ์„ค์ •์„ ์˜ค๋ฒ„๋ผ์ด๋“œํ•œ๋‹ค.

์ด ๋™์ž‘๊ณผ ๋™์ผํ•˜๋„๋ก ์ฝ”๋“œ๋ฅผ ์‚ด์ง ์ˆ˜์ •ํ•ด๋ณด์ž.

import { useEffect, useState } from "react";
import { useLocalStorage, useMediaQuery } from "usehooks-ts";

const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)";

const THEME_MODE = {
  DARK: "dark",
  LIGHT: "light",
} as const;

const CONTROL_MODE = {
  USER: "user",
  OS_DEFAULT: "os_default",
} as const;

type ThemeMode = (typeof THEME_MODE)[keyof typeof THEME_MODE];
type ControlMode = (typeof CONTROL_MODE)[keyof typeof CONTROL_MODE];

type UseAppThemeOutput = {
  mode: ThemeMode;
  toggle: () => void;
  setLight: () => void;
  setDark: () => void;
  setMode: (mode: ThemeMode) => void;
  setControl: (control: ControlMode) => void;  /** ๐Ÿ’– ์ถ”๊ฐ€ : user/os ์—๊ฒŒ ์ œ์–ด๊ถŒ ๋„˜๊ธฐ๊ธฐ */
};

export const useAppTheme = (): UseAppThemeOutput => {
  const isDarkOS = useMediaQuery(COLOR_SCHEME_QUERY);
  const [osPrefersMode] = useState(
    isDarkOS ? THEME_MODE.DARK : THEME_MODE.LIGHT
  );
  /**
  ๐Ÿ’– ์ถ”๊ฐ€ : ์ดˆ๊ธฐ ์ œ์–ด๊ถŒ์€ os๊ฐ€ ์ง€๋‹Œ๋‹ค.
  */
  const [themeMode, setThemeMode] = useLocalStorage<ThemeMode>(
    "app-theme",
    osPrefersMode
  );
  const [themeControl, setThemeControl] = useLocalStorage<ControlMode>(
    "control-theme",
    CONTROL_MODE.OS_DEFAULT
  );

  useEffect(() => {
    const root = document?.documentElement;

    if (themeControl === "os_default") {
      setThemeMode(isDarkOS ? THEME_MODE.DARK : THEME_MODE.LIGHT);
    }
    root.dataset.theme = themeMode;
  }, [isDarkOS, osPrefersMode, themeControl, themeMode]);

  return {
    mode: themeMode,
    toggle: () => {
      setThemeControl("user");
      setThemeMode((previous) =>
        previous === THEME_MODE.DARK ? THEME_MODE.LIGHT : THEME_MODE.DARK
      );
    },
    setLight: () => setThemeMode(THEME_MODE.LIGHT),
    setDark: () => setThemeMode(THEME_MODE.DARK),
    setMode: (mode) => setThemeMode(mode),
    setControl: (control) => setThemeControl(control),
  };
};

 

// Theme button

"use client";
import { MyButton } from "@tripie/design-system";
import { useAppTheme } from "@tripie/hooks";

const ThemeButton = () => {
  const { setControl } = useAppTheme();
  return (
    <MyButton onClick={() => setControl("os_default")}>os default</MyButton>
  );
};

const ToggleButton = () => {
  const { mode, toggle } = useAppTheme();
  return (
    <MyButton onClick={toggle}>
      to {mode === "dark" ? "light" : "dark"}
    </MyButton>
  );
};

ThemeButton.Toggle = ToggleButton;

export default ThemeButton;
// app/page.tsx

"use client";
import ThemeButton from "../components/ThemeButton";

export default function Home() {
  return (
    <div>
      <ThemeButton />
      <ThemeButton.Toggle />

      <h1>this is home</h1>
    </div>
  );
}

os๋‚˜ ์œ ์ €์—๊ฒŒ ๋ชจ๋“œ ์„ ํƒ ์œ„์ž„

๐Ÿ€ ๋งˆ์น˜๋ฉด์„œ

์˜ค๋Š˜์€ ๋‹คํฌ/๋ผ์ดํŠธ ํ…Œ๋งˆ ์ œ์–ด๊ถŒ์„ ์œ ์ €๋‚˜ os์— ๋„˜๊ธฐ๋Š” ๋ฐฉ์‹์— ๋Œ€ํ•ด ์‚ดํŽด๋ดค๋‹ค.
๋ฌผ๋ก  ํ”„๋กœ์ ํŠธ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ์„œ๋น„์Šค์— ๋”ฐ๋ผ์„œ ๋‹คํฌ/๋ผ์ดํŠธ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ด ์ฃผ๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜์ง€ ์•Š์„ ์ˆ˜๋„ ์žˆ์ง€๋งŒ, 
์ž ์ž๊ธฐ ์ „ ์นจ๋Œ€์— ๋ˆ„์›Œ ํฐ์„ ๋ณด๋Š” ์‚ฌ๋žŒ์—๊ฒŒ๋Š” ํ•„์ˆ˜์ง€ ์•Š์„๊นŒ ์‹ถ๋‹ค๐Ÿคญ

๋‹คํฌ ๋ชจ๋“œ ๊ตฌํ˜„์— ๊ด€ํ•ด ์นด์นด์˜ค FE ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ๋Š” body ํƒœ๊ทธ์— ํด๋ž˜์Šค๋ฅผ ๋ถ™์ด๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ๋ฅผ ์ถ”์ฒœํ•˜์ง€๋งŒ ํ˜„์žฌ ์ฝ”๋“œ์—์„œ๋Š” html์— ๋ถ™์ด๊ณ  ์žˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  body ํƒœ๊ทธ ๋ฐ”๋กœ ์•„๋ž˜์— ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด๋ฅผ ๊ฐ์‹ธ๊ณ  ์žˆ๋Š” section์— ํด๋ž˜์Šค๋ฅผ ๋ถ€์—ฌํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.
์ด๋Š” body ํƒœ๊ทธ๊ฐ€ app/layout.tsx ์™ธ์—๋Š” ๋“ฑ์žฅํ•  ์ˆ˜ ์—†๋‹ค๋Š” next์˜ ์ œ์•ฝ(๊ธ€ ์ž‘์„ฑ ๊ธฐ์ค€)๋•Œ๋ฌธ์— ์„ ํƒํ•œ ๋ฐฉ์‹์ด๋‹ค.

ํ•œํŽธ html์— ์ปฌ๋Ÿฌ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ด ์ค˜์„œ document์˜ ์ตœ์ƒ๋‹จ์ด๋ฏ€๋กœ ๋ชจ๋“  ์Šคํƒ€์ผ์ด cascade ๋˜๊ณ , spa route ๋ณ€๊ฒฝ์ด๋‚˜ full reload์‹œ ๊นœ๋นก๊ฑฐ๋ฆผ์„ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์—์„œ ์ข‹์€ ๊ฑฐ ๊ฐ™๋‹ค.

 


๐Ÿ“š References

 

https://fe-developers.kakaoent.com/2021/211118-dark-mode/

 

์›น์—์„œ ๋‹คํฌ๋ชจ๋“œ ์ง€์›ํ•˜๊ธฐ | ์นด์นด์˜ค์—”ํ„ฐํ…Œ์ธ๋จผํŠธ FE ๊ธฐ์ˆ ๋ธ”๋กœ๊ทธ

๋ฐฉ๊ฒฝ๋ฏผ(kai) ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ๋ณด์ด๋Š” ๋ถ€๋ถ„์„ ๊ฐœ๋ฐœํ•œ๋‹ค๋Š” ๋ฐ์„œ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž์˜ ๋งค๋ ฅ์„ ๋“ฌ๋ฟ ๋Š๋ผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

fe-developers.kakaoent.com