1. Layout 컴포넌트
페이지마다 Header, Footer를 직접 넣으면 나중에 바꿀 때 전부 수정해야 합니다.
그러나, Layout으로 감싸면 한 곳에서 관리할 수 있습니다.
- 프로젝트의 기본적인 화면 구조를 잡을 수 있음
- 반복적으로 들어가야 하는 헤더, 푸터 등을 매 화면마다 제공
- 상황과 필요에 따라 레이아웃이 변경될 수 있도록 대비할 수 있음
📄 Layout.tsx
import Footer from "../common/Footer";
import Header from "../common/Header";
interface LayoutProps {
children: React.ReactNode;
}
function Layout({ children }: LayoutProps) {
return (
<>
<Header />
<main>{children}</main>
<Footer />
</>
)
}
export default Layout;
📄 App.tsx
import Layout from "./components/layout/Layout";
import Home from "./pages/Home";
function App() {
return (
<Layout>
<Home />
</Layout>
);
}
export default App;
children 타입
LayoutProps에서 사용하는 children 타입은 계층 구조를 가집니다.
ReactNode ⊃ ReactElement ⊃ JSX.Element
- React.ReactNode: 가장 넓은 범위. null, undefined, 숫자, 문자열, ReactElement 등을 모두 포함합니다. 가장 권장되는 타입입니다.
- React.ReactElement: 오직 리액트 컴포넌트(요소)만 허용합니다. (문자열 등은 불가)
- JSX.Element: 리액트 요소의 실행 결과 타입입니다.
🚨 children을 ReactNode로 설정하면 어떤 값이든 유연하게 받을 수 있지만, 특정 컴포넌트만 강제하고 싶을 때는 더 좁은 타입을 고려해야 합니다. 하지만 일반적인 레이아웃에서는 ReactNode가 표준입니다.
2. CSS Reset & Normalize: 평탄화 작업
브라우저마다 기본적으로 적용된 스타일이 다르기 때문에, 이를 초기화하여 동일한 화면을 보장해야합니다.
주요 라이브러리
| Eric Meyer's reset.css | 모든 기본 스타일 제거. 처음부터 전부 직접 써야 함 |
| normalize.css | 브라우저 차이만 통일. 쓸만한 기본값은 남김 |
| sanitize.css ✅ | normalize 기반인데 실용적인 기본값 추가로 들어가 있음 최신 브라우저 경향 반영, :where 셀렉터 활용으로 명시도 문제 해결 |
💡 :where
sanitize.css가 사용하는 :where는 명시도(Specificity)가 0입니다. 덕분에 전역 스타일을 적용하면서도, 우리가 나중에 작성할 커스텀 CSS가 아주 쉽게 덮어쓸 수 있도록 도와줍니다.
— sanitize.css
저는 이것들 중 :where형식을 사용해보고 싶어서 sanitize.css 을 선택하였습니다. 이전의 프로젝트에서는 !import를 남발했던 기억이 있어서, 제 상황에서 필요해보였습니다.
npm install sanitize.css --save
// index.tsx (엔트리 포인트에 한 번만)
import 'sanitize.css';

이를 진행하면, 개발자 도구에서 확인하면 :where(h1) 같은 셀렉터가 생긴 걸 볼 수 있습니다.
3. 왜 CSS-in-JS (styled-components) 인가?
전통적인 CSS 방식의 한계를 극복하기 위해 등장했습니다.
- 전역 충돌 문제
- 클래스명이 고유한 난수로 생성되어 스타일이 겹치지 않습니다.
- 상태 공유 어려움
- JavaScript의 Props를 스타일 코드에서 직접 사용할 수 있습니다.
- 로딩 순서 / 명시도
- 라이브러리 CSS가 내 코드보다 나중에 로드되면 현재 내 스타일이 덮어씌어지는 등 React 환경에서는 렌더 순서에 따라 예측 불가능해집니다.
- → 관심사의 분리 (Separation of Concerns)
: CSS-in-JS는 컴포넌트 단위로 JS+CSS+HTML을 같은 곳에 모아서 캡슐화합니다.
- → 관심사의 분리 (Separation of Concerns)
- 라이브러리 CSS가 내 코드보다 나중에 로드되면 현재 내 스타일이 덮어씌어지는 등 React 환경에서는 렌더 순서에 따라 예측 불가능해집니다.
styled-components 을 적용해보자.
npm install styled-components --save
📄 Header.tsx
import { styled } from "styled-components";
function Header() {
return (
<HeaderStyle>
<h1>book store</h1>
</HeaderStyle>
);
}
const HeaderStyle = styled.header`
background-color: #333;
h1 {
color: white;
}
`;
export default Header;
Styled-components가 제공하는 전역 도구
- createGlobalStyle
- 전체 앱에 적용할 기본 스타일을 createGlobalStyle로 만들 수 있습니다.
- ThemeProvider (아래 theme 구현에서 사용)
- Context API 기반의 상위 컴포넌트
- 데이터 전달: 내부적으로 리액트의 Context를 사용하여 theme 객체를 모든 하위 styled-components에 주입합니다.
- 동적 테마: theme prop에 전달되는 객체가 바뀌면, 이를 구독하고 있는 모든 스타일드 컴포넌트가 자동으로 리렌더링되어 스타일이 즉시 변경됩니다. (다크모드 구현의 핵심)
- 중첩 가능: 여러 개의 ThemeProvider를 중첩하여 특정 구역만 다른 테마를 적용할 수도 있습니다.
📄 global.ts
import {createGlobalStyle} from 'styled-components';
// @ts-ignore
import 'sanitize.css';
import { ThemeName } from './theme';
interface Props {
themeName: ThemeName;
}
export const GlobalStyle = createGlobalStyle<Props>`
body {
margin: 0;
padding: 0;
background-color : ${(props) => (props.themeName === "light" ? "white" : "black")}
}
h1 {
margin: 0;
}
* {
color: ${(props) => (props.themeName === "light" ?
"black" : "white")};
}
`;
3. 테마 스위처 (Theme Switcher) 구현하기
Theme 시스템
색상값을 컴포넌트마다 하드코딩하면 나중에 디자인 바꿀 때 전부 수정해야 하기때문에, theme 객체로 중앙 관리합니다.
// style/theme.ts
export 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: 'lightgray',
secondary: 'blue',
third: 'green',
},
};
export const dark: Theme = {
name: 'dark',
color: {
primary: 'coral',
background: 'midnightblue',
secondary: 'darkblue',
third: 'darkgreen',
},
};
export const getTheme = (themeName: ThemeName): Theme => {
switch (themeName) {
case "light": return light;
case "dark": return dark;
}
};
styled 컴포넌트에서 theme를 꺼내쓸 수 있습니다.
const HeaderStyle = styled.header`
background: ${({ theme }) => theme.color.background};
h1 {
color: ${({ theme }) => theme.color.primary};
}
`;
GlobalStyle에 theme 적용 (themeName prop 전달)
// style/global.ts
import { createGlobalStyle } from 'styled-components';
import { ThemeName } from './theme';
interface Props {
themeName: ThemeName;
}
export const GlobalStyle = createGlobalStyle<Props>`
body {
color: ${({ themeName }) => themeName === 'light' ? 'black' : 'white'};
}
`;
Context API로 전역 테마 관리
왜 Context가 필요한가
App.tsx에서 useState로 테마 관리하면 ThemeSwitcher, Header 등 하위 컴포넌트에서 테마 바꾸려 할 때 props로 계속 내려줘야 합니다. 여기서 Context를 쓰면 Provider 하위 어디서든 바로 꺼내 쓸 수 있습니다.
- createContext(): 컨텍스트 공간 생성
- <Context.Provider>: 하위 컴포넌트들에게 값 제공
- useContext(): 어디서든 값 꺼내 쓰는 훅
BookStoreThemeProvider
styled-components ThemeProvider, GlobalStyle, 커스텀 Context를 하나로 묶었습니다. App.tsx를 깔끔하게 유지할 수 있고 테마 관련 로직이 한 곳에 모일 수 있게 했습니다.
// 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_key";
interface State {
themeName: ThemeName;
toggleTheme: () => void;
}
export const ThemeContext = createContext<State>({
themeName: "light" as ThemeName,
toggleTheme: () => {},
});
export const BookStoreThemeProvider = ({ children }: { children: ReactNode }) => {
const [themeName, setThemeName] = useState<ThemeName>("light");
// 초기 로드 시 저장된 테마 불러오기
useEffect(() => {
const savedThemeName = localStorage.getItem(THEME_LOCALSTORAGE_KEY) as ThemeName;
setThemeName(savedThemeName || DEFAULT_THEME_NAME);
}, []);
// 테마 토글 및 로컬스토리지 저장
const toggleTheme = () => {
const next = themeName === "light" ? "dark" : "light";
setThemeName(next);
localStorage.setItem(THEME_LOCALSTORAGE_KEY, next);
};
return (
<ThemeContext.Provider value={{ themeName, toggleTheme }}>
<ThemeProvider theme={getTheme(themeName)}>
<GlobalStyle themeName={themeName} />
{children}
</ThemeProvider>
</ThemeContext.Provider>
);
};
ThemeSwitcher
// components/header/ThemeSwitcher.tsx
import { useContext } from "react";
import { ThemeContext } from "../../context/themeContext";
function ThemeSwitcher() {
const { themeName, toggleTheme } = useContext(ThemeContext);
return <button onClick={toggleTheme}>{themeName}</button>;
}
export default ThemeSwitcher;
App.tsx 최종
// 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;
💡 주의: useContext(ThemeContext)는 반드시 BookStoreThemeProvider 안에 있는 컴포넌트에서만 써야 합니다 App.tsx 자체는 Provider 밖이라 거기서 useContext 쓰면 기본값(toggleTheme: ()=>{})만 나옵니다.
'Devcourse > BookShop 사이드 프로젝트' 카테고리의 다른 글
| [BookShop-FE] 회원가입 페이지 구현, React Hook Form (0) | 2026.04.19 |
|---|---|
| [BookShop-FE] 본격적인 UI 작업 시작 - 헤더와 푸터 (0) | 2026.04.17 |
| [BookShop 프로젝트] 백엔드 마무리하기 (0) | 2026.03.18 |
| [BookShop 프로젝트] jwt 토큰 추가, jwt 예외처리, try...catch, throw (0) | 2026.03.04 |
| [BookShop 프로젝트] 주문 API - LAST_INSERT_ID(), MAX()로 최신 데이터 가져오기, insertId, 벌크 INSERT (0) | 2026.02.20 |