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