🎯 λ ˆμ΄μ•„μ›ƒ ꡬ성을 작고 sanitize.css와 styled-componentsλ₯Ό μ‚¬μš©ν•΄μ„œ ν…Œλ§ˆλ₯Ό μ μš©μ‹œν‚΅λ‹ˆλ‹€.


πŸ“— Today I Learned

λ ˆμ΄μ•„μ›ƒ ꡬ성

λ ˆμ΄μ•„μ›ƒμ€ μ½˜ν…μΈ λ₯Ό ν™”λ©΄μ΄λ‚˜ νŽ˜μ΄μ§€μ— λ°°μΉ˜ν•˜λŠ” κ΅¬μ‘°λ‚˜ 방식을 λ§ν•©λ‹ˆλ‹€.

Home Page Layout

🏠 Home Page
β”œβ”€β”€ <Header/>         ← 상단 헀더 μ˜μ—­ (곡톡 μ»΄ν¬λ„ŒνŠΈ)
β”œβ”€β”€ <Home/>           ← 메인 μ½˜ν…μΈ  μ˜μ—­ (ν™ˆ ν™”λ©΄ μ „μš©)
└── <Footer/>         ← ν•˜λ‹¨ ν‘Έν„° μ˜μ—­ (곡톡 μ»΄ν¬λ„ŒνŠΈ)

Detail Page Layout

πŸ“„ Detail Page
β”œβ”€β”€ <Header/>          ← 상단 헀더 μ˜μ—­ (곡톡 μ»΄ν¬λ„ŒνŠΈ)
β”œβ”€β”€ μ½˜ν…μΈ  μ˜μ—­
β”‚   β”œβ”€β”€ <Sidebar/>     ← μ™Όμͺ½ μ‚¬μ΄λ“œλ°” (μΆ”κ°€ 메뉴 λ“±)
β”‚   └── 상세 μ»΄ν¬λ„ŒνŠΈ    ← 였λ₯Έμͺ½ 상세 μ½˜ν…μΈ  (νŽ˜μ΄μ§€ λ‚΄μš©)
└── <Footer/>          ← ν•˜λ‹¨ ν‘Έν„° μ˜μ—­ (곡톡 μ»΄ν¬λ„ŒνŠΈ)

πŸ€” μ™œ λ ˆμ΄μ•„μ›ƒμ„ ꡬ성해야 ν• κΉŒ?

  • ν™”λ©΄ ꡬ쑰λ₯Ό 섀계할 수 μžˆμŠ΅λ‹ˆλ‹€.
  • Header, Footer 같은 곡톡 μš”μ†Œλ₯Ό ν•œ λ²ˆμ— 관리할 수 μžˆμŠ΅λ‹ˆλ‹€.
  • λ‹€μ–‘ν•œ 상황에 맞게 μœ μ—°ν•˜κ²Œ 화면을 λ°”κΏ€ 수 μžˆμŠ΅λ‹ˆλ‹€.
    좜처: MDN

μ½”λ“œ μ˜ˆμ‹œ

import Footer from '../common/Footer';
import Header from '../common/Header';

interface LayoutProps {
  children: React.ReactNode; // μžμ‹ μš”μ†Œλ₯Ό λ°›μ•„μ˜¬ 수 μžˆλ„λ‘ νƒ€μž… μ„€μ •
}

export default function Layout({ children }: LayoutProps) {
  return (
    <>
      <Header />
      <main>{children}</main>
      <Footer />
    </>
  );
}
  • React.ReactNode : Reactμ—μ„œ λ Œλ”λ§ κ°€λŠ₯ν•œ λͺ¨λ“  νƒ€μž…μ„ μΌμ»«μŠ΅λ‹ˆλ‹€. (ex. λ¬Έμžμ—΄, 숫자, JSX, μ»΄ν¬λ„ŒνŠΈ, λ°°μ—΄ λ“±)
    • React.ReactNode > ReactElement > JSX.Element

μ‚¬μš© 방법 1️⃣ - μš”μ†Œλ‘œ λ„£κΈ°

export default function App() {
  return (
    <Layout children={<Home />} />
  );
}

μ‚¬μš© 방법 2️⃣ - μ»΄ν¬λ„ŒνŠΈ 사이에 λ„£κΈ°

export default function App() {
  return (
    <Layout>
      <Home /> // children
    </Layout>
  );
}

λ ˆμ΄μ•„μ›ƒ μ»΄ν¬λ„ŒνŠΈλŠ” μ–΄λ–€ νŽ˜μ΄μ§€λ“  <Layout> μ•ˆμ— λ„£κΈ°λ§Œ ν•˜λ©΄ Header와 FooterλŠ” μžλ™μœΌλ‘œ ν¬ν•¨λ©λ‹ˆλ‹€.




Global Style

Global Styleμ΄λž€ ν”„λ‘œμ νŠΈ 전체에 μ μš©λ˜λŠ” μ „μ—­ μŠ€νƒ€μΌμ„ λ§ν•©λ‹ˆλ‹€.

πŸ€” μ™œ Global Style을 μž‘μ„±ν• κΉŒ?

user agent stylesheet둜 ν‘œμ‹œλ˜λŠ” λΈŒλΌμš°μ €λ§ˆλ‹€ λ‹€λ₯Έ κΈ°λ³Έ μŠ€νƒ€μΌ 차이λ₯Ό μ—†μ• κΈ° μœ„ν•΄ μ‚¬μš©ν•©λ‹ˆλ‹€.



CSS μ΄ˆκΈ°ν™” 도ꡬ

  • Reset CSS
    λͺ¨λ“  μš”μ†Œμ˜ κΈ°λ³Έ μŠ€νƒ€μΌμ„ μ—†μ• κ³  μ™„μ „νžˆ μ΄ˆκΈ°ν™” ν•©λ‹ˆλ‹€.

  • normalize.css
    λΈŒλΌμš°μ € κ°„ μŠ€νƒ€μΌ 차이λ₯Ό μ—†μ• κ³ , 의미 μžˆλŠ” κΈ°λ³Έ μŠ€νƒ€μΌμ€ μœ μ§€ν•˜λ©° μ΄ˆκΈ°ν™” ν•©λ‹ˆλ‹€.

  • ✨ sanitize.css
    μ‚¬μš©μžμ˜ 접근성을 ν•΄μΉ˜μ§€ μ•ŠμœΌλ©΄μ„œλ„ κΈ°λ³Έ λΈŒλΌμš°μ € μŠ€νƒ€μΌ 차이λ₯Ό 정리해주기 λ•Œλ¬Έμ—, 많이 μ‚¬μš©λ˜κ³  μžˆμŠ΅λ‹ˆλ‹€.



styled-components πŸ’…

styled-componentsλŠ” CSS-in-JS λ°©μ‹μ˜ 라이브러리둜 JavaScript 파일 μ•ˆμ—μ„œ μŠ€νƒ€μΌμ„ μ •μ˜ν•˜κ³ , 이λ₯Ό React μ»΄ν¬λ„ŒνŠΈμ²˜λŸΌ μ‚¬μš©ν•  수 있게 ν•΄μ£ΌλŠ” λ„κ΅¬μž…λ‹ˆλ‹€.

<npm install styled-components

πŸ€” μ™œ CSS-in-JS λ°©μ‹μœΌλ‘œ μž‘μ„±ν• κΉŒ?

κΈ°μ‘΄ CSS 파일 μž‘μ„±μ—λŠ” λͺ‡ κ°€μ§€ λ‹¨μ πŸ‘Žμ΄ μžˆμŠ΅λ‹ˆλ‹€.

  • μ „μ—­ μŠ€νƒ€μΌ 좩돌
  • μ»΄ν¬λ„ŒνŠΈ κ°„μ˜ μŠ€νƒ€μΌ μž¬μ‚¬μš©μ˜ 어렀움
  • 클래슀λͺ… 쀑볡, μ˜€λ²„λΌμ΄λ”© λ³΅μž‘ν•¨
  • CSS μˆœμ„œ 문제둜 μš°μ„ μˆœμœ„ κΌ¬μž„ λ°œμƒ

πŸ’‘ μ΄λŸ¬ν•œ λ¬Έμ œμ μ„ ν•΄κ²° ν•˜κΈ° μœ„ν•΄ CSS-in-JS 방식이 λ“±μž₯ν–ˆμŠ΅λ‹ˆλ‹€. λŒ€ν‘œμ μœΌλ‘œ styled-components 같은 라이브러리λ₯Ό μ‚¬μš©ν•˜λ©΄ λ‹€μŒκ³Ό 같은 μž₯μ πŸ‘μ΄ μƒκ²¨λ‚©λ‹ˆλ‹€.

  • μŠ€νƒ€μΌμ„ μ»΄ν¬λ„ŒνŠΈν™”ν•˜μ—¬ μ½”λ“œ ꡬ쑰가 λͺ…확해짐
  • μž¬μ‚¬μš©μ„± λ†’κ³  μœ μ§€λ³΄μˆ˜ 용이
  • 동적 μŠ€νƒ€μΌλ§μ΄ 쉬움


Global Style의 적용 방법

sanitize.css와 styled-componentsλ₯Ό μ‚¬μš©ν•΄μ„œ μŠ€νƒ€μΌμ„ μ μš©ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

// style/global.ts
import 'sanitize.css';
import { createGlobalStyle } from 'styled-components';

export const GlobalStyle = createGlobalStyle`
  body {
    padding: 0;
    margin: 0;
  }

  h1 {
    margin: 0;
  }
`;
// App.tsx
import { GlobalStyle } from './styles/GlobalStyle';

function App() {
  return (
    <>
      <GlobalStyle />
      <Layout>
        <Home />
      </Layout>
    </>
  );
}

GlobalStyle은 보톡 App.tsx μ΅œμƒλ‹¨μ—μ„œ ν•œ 번만 μ μš©ν•©λ‹ˆλ‹€.




ν…Œλ§ˆ(Theme)

ν…Œλ§ˆλž€ μ›Ήμ‚¬μ΄νŠΈμ— μ‚¬μš©λ˜λŠ” 색상, 폰트, 간격 λ“±μ˜ μ‹œκ°μ  μš”μ†Œλ“€μ„ ν•œ κ³³μ—μ„œ κ΄€λ¦¬ν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.

πŸ€” μ™œ ν…Œλ§ˆλ₯Ό μ‚¬μš©ν• κΉŒ?

  • UX/UI 일관성 μœ μ§€
  • μœ μ§€λ³΄μˆ˜μ™€ κΈ°λŠ₯ ν™•μž₯에 유리
  • μž¬μ‚¬μš©μ„±κ³Ό 가독성 증가
  • μ‚¬μš©μž 맞좀 μ„€μ • κ°€λŠ₯

μ½”λ“œ μ˜ˆμ‹œ

type ThemeName = 'light' | 'dark';
type ColorKey = 'primary' | 'background' | 'secondary' | 'third';

interface Theme {
  name: ThemeName;
  color: Record<ColorKey, string>;
}

export const light: Theme = {
  name: 'light',
  color: {
    primary: 'brown',
    background: 'lightgrey',
    secondary: 'blue',
    third: 'green',
  },
};

export const dark: Theme = {
  name: 'dark',
  color: {
    primary: 'coral',
    background: 'midnightblue',
    secondary: 'darkblue',
    third: 'darkgreen',
  },
};
  • Record<ColorKey, string>둜 인해 colorλŠ” λ‹€μŒκ³Ό 같은 ꡬ쑰λ₯Ό κ°–μŠ΅λ‹ˆλ‹€.
color: {
  primary: string;
  background: string;
  secondary: string;
  third: string;
}

⚠️ color κ°μ²΄λŠ” λ°˜λ“œμ‹œ 4개의 ν‚€λ₯Ό λͺ¨λ‘ κ°€μ§€κ³  μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.

κ°•μ œν•˜λŠ” μ΄μœ λŠ” μ»΄ν¬λ„ŒνŠΈλ₯Ό λ§Œλ“€ λ•Œ ν…Œλ§ˆ 색상을 μ‚¬μš©ν•˜λŠ” λΆ€λΆ„μ—μ„œλ„ μ˜ˆμ™Έ 없이 μΌκ΄€λœ μŠ€νƒ€μΌμ„ 쀄 수 있기 λ•Œλ¬Έμž…λ‹ˆλ‹€.

const SomeComponent = styled.div`
  color: ${({ theme }) => theme.color.primary};
  background: ${({ theme }) => theme.color.background};
`;



Context API

Context APIλŠ” React의 μ „μ—­ μƒνƒœ 관리 λ„κ΅¬λ‘œ μ»΄ν¬λ„ŒνŠΈλ₯Ό μ—¬λŸ¬ 단계 λ‚΄λ €κ°€μ§€ μ•Šκ³ λ„ 데이터λ₯Ό κ³΅μœ ν•  수 있게 ν•΄μ€λ‹ˆλ‹€.


ν…Œλ§ˆ μŠ€μœ„μ²˜ πŸŒ“

Context APIλ₯Ό ν™œμš©ν•΄ ν…Œλ§ˆ μŠ€μœ„μ²˜λ₯Ό λ§Œλ“­λ‹ˆλ‹€.

  • μ‚¬μš©μžλŠ” ν† κΈ€ UIλ₯Ό 톡해 라이트/닀크 ν…Œλ§ˆλ₯Ό μ „ν™˜ν•  수 μžˆλ„λ‘ μ„€μ •ν•©λ‹ˆλ‹€.

  • ν…Œλ§ˆ μƒνƒœλŠ” Context둜 κ΄€λ¦¬ν•˜μ—¬ 전체 μ»΄ν¬λ„ŒνŠΈμ—μ„œ μ ‘κ·Ό κ°€λŠ₯ν•˜λ„λ‘ ν•©λ‹ˆλ‹€.

  • μ‚¬μš©μžκ°€ μ„€μ •ν•œ ν…Œλ§ˆλŠ” 둜컬 μŠ€ν† λ¦¬μ§€μ— μ €μž₯ν•˜μ—¬ μƒˆλ‘œκ³ μΉ¨ν•΄λ„ μœ μ§€λ˜κ²Œ ν•©λ‹ˆλ‹€.


πŸ“„ ThemeContext.tsx

import { createContext, ReactNode, useEffect, useState } from 'react';
import { getTheme, ThemeName } from '../style/theme';
import { ThemeProvider } from 'styled-components';
import { GlobalStyle } from '../style/global';

const DEFAULT_THEME_NAME = 'light';
const THEME_LOCALSTORAGE_KEY = 'book_store_theme';

interface State {
  themeName: ThemeName;
  toggleTheme: () => void;
}

export const state = {
  themeName: DEFAULT_THEME_NAME as ThemeName,
  toggleTheme: () => {},
};

export const ThemeContext = createContext<State>(state);

export const BookStoreThemeProvider = ({
  children,
}: {
  children: ReactNode;
}) => {
  const [themeName, setThemeName] = useState<ThemeName>(DEFAULT_THEME_NAME);

  const toggleTheme = () => {
    setThemeName(themeName === 'light' ? 'dark' : 'light');
    localStorage.setItem(
      THEME_LOCALSTORAGE_KEY,
      themeName === 'light' ? 'dark' : 'light'
    );
  };

  useEffect(() => {
    const savedThemeName = localStorage.getItem(
      THEME_LOCALSTORAGE_KEY
    ) as ThemeName;

    setThemeName(savedThemeName || DEFAULT_THEME_NAME);
  }, []);

  return (
    <ThemeContext.Provider value={{ themeName, toggleTheme }}>
      <ThemeProvider theme={getTheme(themeName)}>
        <GlobalStyle themeName={themeName} />
        {children}
      </ThemeProvider>
    </ThemeContext.Provider>
  );
};
  • BookStoreThemeProviderλŠ” 이 ν…Œλ§ˆ μƒνƒœλ₯Ό μ „μ—­μœΌλ‘œ κ³΅κΈ‰ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈμž…λ‹ˆλ‹€.

    • ν˜„μž¬ ν…Œλ§ˆ μƒνƒœλŠ” useState둜 κ΄€λ¦¬ν•©λ‹ˆλ‹€.

    • λ²„νŠΌ 클릭 μ‹œμ— toggleTheme을 ν˜ΈμΆœν•΄ ν…Œλ§ˆλ₯Ό λ°˜μ „μ‹œν‚€κ³ , ν…Œλ§ˆλŠ” localStorage에 μ €μž₯ν•˜μ—¬ μƒˆλ‘œκ³ μΉ¨ν•΄λ„ μœ μ§€λ˜κ²Œ ν•©λ‹ˆλ‹€.

    • useEffectλŠ” μ²˜μŒμ— λ Œλ”λ§λ  λ•Œ([]), 둜컬 μŠ€ν† λ¦¬μ§€μ—μ„œ ν…Œλ§ˆ 값을 λΆˆλŸ¬μ˜΅λ‹ˆλ‹€. 값이 μ—†μœΌλ©΄, κΈ°λ³Έκ°’μœΌλ‘œ lightλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.

    • Context와 styled-components의 ThemeProviderλ₯Ό ν•¨κ»˜ 감싸주어 returnν•΄μ€λ‹ˆλ‹€.

      • ThemeContext.Provider : React λ‚΄λΆ€μ—μ„œ ν…Œλ§ˆ 이름(themeName)κ³Ό ν…Œλ§ˆ λ³€κ²½ ν•¨μˆ˜(toggleTheme)λ₯Ό μ»΄ν¬λ„ŒνŠΈ μ–΄λ””μ„œλ“  μ‚¬μš©ν•  수 있게 ν•΄μ€λ‹ˆλ‹€.

      • ThemeProvider : μŠ€νƒ€μΌμ— μ‚¬μš©λ˜λŠ” ν…Œλ§ˆ 색상듀을 μ μš©μ‹œμΌœμ€λ‹ˆλ‹€.


πŸ“„ ThemeSwitcher.tsx

import { useContext } from 'react';
import { ThemeContext } from '../../context/ThemeContext';

export default function ThemeSwitcher() {
  const { themeName, toggleTheme } = useContext(ThemeContext);

  return <button onClick={toggleTheme}>{themeName}</button>;
}
  • useContext둜 themeNameκ³Ό toggleThemeλ₯Ό λ°›μ•„μ„œ λ²„νŠΌμ„ λ Œλ”λ§ν•©λ‹ˆλ‹€.

πŸ“„ App.tsx

import Layout from './components/layout/Layout';
import Home from './pages/Home';
import ThemeSwitcher from './components/header/ThemeSwitcher';
import { BookStoreThemeProvider } from './context/ThemeContext';

function App() {
  return (
    <BookStoreThemeProvider>
      <ThemeSwitcher />
      <Layout>
        <Home />
      </Layout>
    </BookStoreThemeProvider>
  );
}

export default App;
  • BookStoreThemeProvider μ»΄ν¬λ„ŒνŠΈλŠ” μ•± μ·¨μƒμœ„μ—μ„œ μ‚¬μš©λ˜λ©° 전체 ν…Œλ§ˆ μƒνƒœλ₯Ό κ΄€λ¦¬ν•©λ‹ˆλ‹€.



✏️ 회고

λΈŒλΌμš°μ € μŠ€νƒ€μΌμ„ μ΄ˆκΈ°ν™”ν•˜κ³ , styled-components와 ν…Œλ§ˆ μ‹œμŠ€ν…œ 🌞 / πŸŒ™μ„ μ΄μš©ν•΄μ„œ λ§Œλ“€μ–΄λ³΄λŠ” 게 쉽지 μ•Šμ•˜μ§€λ§Œ, ν…Œλ§ˆ μƒνƒœλ₯Ό μ „μ—­μ—μ„œ κ΄€λ¦¬ν•˜λŠ” μ—°μŠ΅μ„ 더 많이 ν•΄μ•Όν•  것 κ°™λ‹€.

profile
🌱개발 기둝μž₯

0개의 λŒ“κΈ€