Global Style & Theme

Global Style

์ „์—ญ์œผ๋กœ ์Šคํƒ€์ผ์„ ์ง€์ •

createGlobalStyle

์ „์—ญ ์Šคํƒ€์ผ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํŠน๋ณ„ํ•œ ์Šคํƒ€์ผ๋“œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜

์ผ๋ฐ˜์ ์ธ ์Šคํƒ€์ผ๋“œ ์ปดํฌ๋„ŒํŠธ๋Š” ์ž๋™์œผ๋กœ ํด๋ž˜์Šค ๋ฒ”์œ„๊ฐ€ ์ง€์ •๋˜์–ด ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋กœ๋ถ€ํ„ฐ ๊ฒฉ๋ฆฌ๋˜์ง€๋งŒ createGlobalStyle ์€ ์ด๋Ÿฌํ•œ ์ œํ•œ์ด ์—†์–ด์„œ ์ „์—ญ ์Šคํƒ€์ผ๋ง์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

์ „์—ญ ์Šคํƒ€์ผ์— reset css๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ดˆ๊ธฐํ™”๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.

The 62.5% Font Size Trick

// styles/GlobalStyle.ts
import { createGlobalStyle } from 'styled-components';
import reset from 'styled-reset';

const GlobalStyle = createGlobalStyle`
  ${reset}
  html {
    box-sizing: border-box;
  }

  *,
  *::before,
  *::after {
    box-sizing: inherit;
  }

  html {
  // ๋ธŒ๋ผ์šฐ์ €์˜ ํฐํŠธ ํฌ๊ธฐ ์„ค์ •๊ณผ ์—ฐ๋™๋˜๋Š” rem ๋‹จ์œ„๋ฅผ ์‚ฌ์šฉ,
  // rem ๋‹จ์œ„๋ฅผ ๊ณ„์‚ฐํ•˜๊ธฐ ์‰ฝ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด ๊ธฐ๋ณธ ํฐํŠธ๋ฅผ 62.5%(10px)๋กœ ์„ค์ •ํ•œ๋‹ค.
    font-size: 62.5%; // 10px, (100% === 16px)
  }

  body {
    font-size: 1.6rem;
  }

  :lang(ko) {
  // ํ•œ๊ธ€๋กœ ์ž‘์„ฑ๋œ ์š”์†Œ ์ค‘ ํ—ค๋”ฉ ํƒœ๊ทธ์— ์˜๋„์น˜ ์•Š์€ ์ค„๋ฐ”๊ฟˆ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์„ค์ •
    h1, h2, h3 {
      word-break: keep-all;
    }
  }
`;

export default GlobalStyle;
import GlobalStyle from './styles/GlobalStyle';

export default function App() {
  return (
    <>
      <GlobalStyle />
      <Greeting />
    </>
  );
}

Reset CSS๋ž€ ๋ธŒ๋ผ์šฐ์ €์— ๊ธฐ๋ณธ์ ์œผ๋กœ ์„ค์ •๋œ ์Šคํƒ€์ผ์„ ์ œ๊ฑฐํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋œ๋‹ค.

Install

npm i styled-reset

Usage

import { Reset } from 'styled-reset';

export default function App() {
  return (
    <>
      <Reset />
      <Greeting />
    </>
  );
}

Theme

๋””์ž์ธ ์‹œ์Šคํ…œ ์ „์ฒด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์ƒ‰์ƒ, ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ, ์•„์ด์ฝ˜ ๋“ฑ์˜ ๋””์ž์ธ ์š”์†Œ์˜ ์ง‘ํ•ฉ

ํ…Œ๋งˆ๋ฅผ ์ •์˜ํ•  ๋•Œ ๋””์ž์ธ ์‹œ์Šคํ…œ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์ปฌ๋Ÿฌ ํŒ”๋ ˆํŠธ, ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ, ๊ฐ„๊ฒฉ ๋“ฑ์„ ํ† ํฐ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ , ์ด๋ฅผ ์ด์šฉํ•ด์„œ ๋””์ž์ธ ์‹œ์Šคํ…œ ์ „์ฒด์˜ ์ผ๊ด€์„ฑ ์žˆ๋Š” ๋””์ž์ธ์„ ์œ ์ง€ํ•˜๊ณ  ์ž‘์—…์˜ ํšจ์œจ์„ฑ์„ ๋†’ํžŒ๋‹ค.

๋””์ž์ธ ํ† ํฐ(token)์ด๋ž€ ๋””์ž์ธ ์š”์†Œ์˜ ์†์„ฑ ๊ฐ’์„ ๋ณ€์ˆ˜ํ™”ํ•˜์—ฌ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹

Token Naming

๋ˆˆ์— ๋ณด์ด๋Š” ๋‹จํŽธ์ ์ธ ์ •๋ณด๋ฅผ ๋„˜์–ด์„œ, โ€œ์˜๋ฏธโ€์— ์ง‘์ค‘

๋””์ž์ธ ์‹œ์Šคํ…œ ์‚ฌ์šฉ์ž๊ฐ€ ํŠน์ • ํ† ํฐ์˜ ๋””์ž์ธ ์˜๋„์™€ ์‚ฌ์šฉ๋ฒ•์„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ค‘์š”ํ•˜๋‹ค.

์ด๋Ÿฌํ•œ ์ฒ ํ•™์€ ์˜๋„์— ๋”ฐ๋ฅธ ๋””์ž์ธ์„ ๊ฐ€๋Šฅ์ผ€ ํ•œ๋‹ค.

์ปฌ๋Ÿฌ ํ† ํฐ์„ ์˜ˆ๋กœ ๋“ค๋ฉด ์ƒ‰์ƒ ๊ฐ’์— ์–ฝ๋งค์ด์ง€ ์•Š๊ณ  ์˜๋„๊ฐ€ ๋” ์ž˜ ๋‹ด๊ธด ์ถ”์ƒํ™”๋œ ๋„ค์ด๋ฐ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค,

์ถ”ํ›„ ์ƒ‰์ƒ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋Š” ์ƒํ™ฉ์ด ์ƒ๊ฒจ๋„ ์ฝ”๋“œ ๋ณ€๊ฒฝ์ด ์‰ฝ๊ณ , ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ๋†’๋‹ค.

ํ•˜์ง€๋งŒ ์–ด๋–ค ๊ฒฝ์šฐ๋Š” ์˜๋„๊ฐ€ ๋‹ด๊ธด ๋„ค์ด๋ฐ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ค์šธ ์ˆ˜๋„ ์žˆ๋‹ค. ์ค‘์š”ํ•œ๊ฑด ์ผ๊ด€์„ฑ๊ณผ ๊ฐ€๋…์„ฑ์ด๋‹ค.

Theme Provider

์Šคํƒ€์ผ๋“œ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ œ๊ณต๋˜๋Š” ThemeProvider๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ Context API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž์‹๋“ค์—๊ฒŒ ํ…Œ๋งˆ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

import { ThemeProvider } from 'styled-components';

import { GlobalStyle, theme } from '@/styles';

const App = () => (
  <ThemeProvider theme={theme}>
    <GlobalStyle />
    <Greeting />
  </ThemeProvider>
);

export default App;

ํ…Œ๋งˆ ํƒ€์ž… ์„ ์–ธ ๋ณ‘ํ•ฉ

์Šคํƒ€์ผ ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ์ •์˜ ํŒŒ์ผ์— ์„ ์–ธ๋œ ํ…Œ๋งˆ ์ธํ„ฐํŽ˜์ด์Šค ํƒ€์ž…์„ ์‚ฌ์šฉ์ž๊ฐ€ ๋งŒ๋“  ํ…Œ๋งˆ์˜ ํƒ€์ž…์œผ๋กœ ํ™•์žฅํ•˜๋ ค๋ฉด, ์„ ์–ธ ๋ณ‘ํ•ฉ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

์‚ฌ์šฉ์ž๊ฐ€ ์ •์˜ํ•œ ํ…Œ๋งˆ์—์„œ ์—ญ์œผ๋กœ ํƒ€์ž…์„ ์ถ”์ถœํ•ด์„œ ํ™•์žฅํ•˜๋ฉด ํŽธ-ํ•˜๋‹ค.

styled.d.ts

import 'styled-components';

import { defaultTheme } from '@/styles';

type Theme = typeof defaultTheme;

declare module 'styled-components' {
  export interface DefaultTheme extends Theme {}
}

ํƒ€์ž… ์†์„ฑ์„ ํ™•์žฅํ•˜๋Š” ๊ฒƒ์„ ์„ ์–ธ ๋ณ‘ํ•ฉ(declaration merging)์ด๋ผ ํ•œ๋‹ค. ๊ฐ™์€ interface๋ฅผ ์žฌ์„ ์–ธ ํ•จ์œผ๋กœ์จ ํƒ€์ž… ์†์„ฑ์„ ์ถ”๊ฐ€์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค. โ†’ ๋ณด๊ฐ•

.d.ts๋Š” TypeScript์—์„œ ํƒ€์ž… ์ •์˜(type definition)๋ฅผ ์œ„ํ•œ ํŒŒ์ผ ํ™•์žฅ์ž,

declare module์€ Typescript์—์„œ ์™ธ๋ถ€ ๋ชจ๋“ˆ์— ๋Œ€ํ•œ ํƒ€์ž… ์„ ์–ธ์„ ์ œ๊ณตํ•˜๋Š” ๊ตฌ๋ฌธ

DarkMode ๊ตฌํ˜„ํ•˜๊ธฐ

ํ…Œ๋งˆ๋ฅผ ๊ฐˆ์•„๋ผ์šฐ๋Š” ์‹์œผ๋กœ ๋‹คํฌ๋ชจ๋“œ๋ฅผ ๊ตฌํ˜„ ๊ฐ€๋Šฅ

useDarkTheme

import { useLocalStorage, useMediaQuery, useUpdateEffect } from 'usehooks-ts'

const COLOR_SCHEME_QUERY = '(prefers-color-scheme: dark)'

interface UseDarkModeOutput {
  isDarkMode: boolean
  toggle: () => void
  enable: () => void
  disable: () => void
}

function useDarkMode(defaultValue?: boolean): UseDarkModeOutput {
  const isDarkOS = useMediaQuery(COLOR_SCHEME_QUERY)
  const [isDarkMode, setDarkMode] = useLocalStorage<boolean>(
    'usehooks-ts-dark-mode',
    defaultValue ?? isDarkOS ?? false,
  )

  // Update darkMode if os prefers changes
  useUpdateEffect(() => {
    setDarkMode(isDarkOS)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isDarkOS])

  return {
    isDarkMode,
    toggle: () => setDarkMode(prev => !prev),
    enable: () => setDarkMode(true),
    disable: () => setDarkMode(false),
  }
}

export default useDarkMode
export default function App() {
const { isDarkMode, toggle } = useDarkMode();

const theme = isDarkMode ? darkTheme : defaultTheme;

return (
  <ThemeProvider theme={theme}>
    <GlobalStyle />
    <Greeting />
    <Button onClick={toggle}>
      Dark Theme Toggle
    </Button>
  </ThemeProvider>
  );
}

jest 'window.matchMedia' ์˜ค๋ฅ˜

jest์—์„œ ์‚ฌ์šฉ๋˜๋Š” jsdom์—์„œ ๊ตฌํ˜„๋˜์ง€ ์•Š์€ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค.

์ด ๊ฒฝ์šฐ ๋ชจํ‚น์„ ํ†ตํ•ด ํ•ด๊ฒฐ์ด ๊ฐ€๋Šฅํ•˜๋‹ค

setupTests.ts

Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation((query) => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(), // deprecated
    removeListener: jest.fn(), // deprecated
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

Last updated