2024년 11월 11일
레이아웃 왜 필요할까
레이아웃 컴포넌트 만들기
// components / Header.tsx
const Header = () => {
return (
<header>
<h1>book store</h1>
</header>
)
}
export default Header;
// components / Footer.tsx
const Footer = () => {
return (
<>
<hr />
<footer>copyright(c), 2024, book store.</footer>
</>
)
}
export default Footer;
// layout / Layout.tsx
import Footer from '../common/Footer';
import Header from '../common/Header';
interface LayoutProps {
children: React.ReactNode; // 리액트로 만든 모든 컴포넌트들이 배치될 수 있다
}
const 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 (
// 1번 방법
<Layout children={<Home />} />
/** 2번 방법
<Layout>
<Home /> <= children
</Layout>
*/
);
}
export default App;
// pages / Home.tsx
const Home = () => {
return (
<>
<div>home body</div>
</>
);
}
export default Home;
global style
global = 프로젝트 전체에 적용 = 프로젝트에 일관된 스타일링을 적용
“user agen stylesheet”로 표시되는 브라우저의 기본 스타일이 차이를 만든다
브라우저 간의 스타일 차이를 극복하기 위해 사용
에릭마이어의 reset css (모든걸 reset 한다는 개념의 css - h1, h2, h3 의 폰트 사이즈가 같아짐)
normalize.css (각 엘리먼트의 고유한 속성을 유지하면서 기기와 브라우저간의 차이를 줄이는데 목적을 둠 - h1, h2, h3의 계층은 그대로 유지됨)
sanitize.css (normalize.css의 업그레이드 버전) → 사용 예정
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import "sanitize.css";
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
styled component
css-in-js는 왜 필요할까
관심사의 분리(Separate of Concerns)
설치
Header 스타일링
// components / Header.tsx
import { styled } from 'styled-components';
const Header = () => {
return (
<HeaderStyle>
<h1>book store</h1>
</HeaderStyle>
)
}
const HeaderStyle = styled.header`
background-color: #333;
h1 {
color: white;
}
`
export default Header;
글로벌 스타일 적용하기
// style / global.ts
import "sanitize.css";
import { createGlobalStyle } from 'styled-components';
export const GlobalStyle = createGlobalStyle`
body {
padding: 0;
margin: 0;
}
h1 {
margin: 0;
}
`;
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { GlobalStyle } from './style/global';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<GlobalStyle />
<App />
</React.StrictMode>
);
테마를 사용하는 이유
sytled-components theme 구성
// style / theme.ts
// 타입 가드
export type ThemeName = "light" | "dark";
type ColorKey = "primary" | "background" | "secondary" | "third";
interface Theme {
name : ThemeName;
colors :
// 1번 방법 (키 제한)
// {[key in ColorKey] : string;}
// 2번 방법 (레코드방식 지정)
Record<ColorKey, string>;
}
export const light: Theme = {
name : "light",
colors : {
primary : "brown",
background : "lightgray",
secondary : "blue",
third : "green",
},
};
export const dark: Theme = {
name : "dark",
colors : {
primary : "coral",
background: "midnightblue",
secondary : "darkblue",
third : "darkgreen"
// secondary, third가 없으면 에러가 남 -> 넣어주면 에러 X
// light 테마에서 여러가지 디자인을 수정하고 dark 테마에 적용해
// 놓지 않는다면 사용자가 테마를 바꿨을때 오류가 생김 -> 따라서 타입 가드 필요
}
}
// App.tsx
import Layout from './components/layout/Layout';
import Home from './pages/Home';
import { GlobalStyle } from './style/global';
import { ThemeProvider } from 'styled-components';
import { light } from './style/theme';
function App() {
return (
<ThemeProvider theme={light}>
<GlobalStyle themeName='light'/> // 이 부분 state로 나중에 관리 예정
<Layout>
<Home />
</Layout>
</ThemeProvider>
/** 2번 방법
<Layout children={<Home />} />
*/
);
}
export default App;
// components / Header.tsx
import { styled } from 'styled-components';
const Header = () => {
return (
<HeaderStyle>
<h1>book store</h1>
</HeaderStyle>
)
}
const HeaderStyle = styled.header`
background-color: ${({theme}) => theme.colors.background};
h1 {
color: ${({theme}) => theme.colors.primary};
}
`
export default Header;
// style / global.ts
import "sanitize.css";
import { createGlobalStyle } from 'styled-components';
import { ThemeName } from './theme';
interface Props {
themeName : ThemeName
}
export const GlobalStyle = createGlobalStyle<Props>`
body {
padding: 0;
margin: 0;
}
h1 {
margin: 0;
}
*{
color : ${(props) => (props.themeName === "light" ? "black" : "white")};
}
`;
기능과 목적
테마 토글 스위치(지역 상태 themeName)
// App.tsx
import Layout from './components/layout/Layout';
import Home from './pages/Home';
import { GlobalStyle } from './style/global';
import { ThemeProvider } from 'styled-components';
import { ThemeName, getTheme } from './style/theme';
import ThemeSwitcher from './components/header/ThemeSwitcher';
import { useState } from 'react';
function App() {
const [themeName, setThemeName] = useState<ThemeName>("light");
return(
<ThemeProvider theme={getTheme(themeName)}>
<GlobalStyle themeName={themeName} />
<ThemeSwitcher themeName={themeName} setThemeName={setThemeName} />
<Layout>
<Home />
</Layout>
</ThemeProvider>
/** 2번 방법
<Layout children={<Home />} />
*/
);
}
export default App;
// components / header / ThemeSwitcher.tsx
import { ThemeName } from '../../style/theme';
interface Props {
themeName: ThemeName;
setThemeName: (themeName: ThemeName) => void;
}
const ThemeSwitcher = ({themeName, setThemeName} : Props) => {
const toggleTheme = () => {
setThemeName(themeName === "light" ? "dark" : "light")
}
return (
<button onClick={toggleTheme}>{themeName}</button>
)
}
export default ThemeSwitcher;
// style / theme.ts
// 타입 가드
export type ThemeName = "light" | "dark";
type ColorKey = "primary" | "background" | "secondary" | "third";
interface Theme {
name: ThemeName;
colors: Record<ColorKey, string>;
}
export const light: Theme = {
name : "light",
colors : {
primary : "brown",
background : "lightgray",
secondary : "blue",
third : "green",
},
};
export const dark: Theme = {
name : "dark",
colors : {
primary : "coral",
background: "midnightblue",
secondary : "darkblue",
third : "darkgreen"
},
};
export const getTheme = (themeName : ThemeName) : Theme => {
switch(themeName) {
case "light" :
return light;
case "dark" :
return dark;
}
}
// style / global.ts
import "sanitize.css";
import { createGlobalStyle } from 'styled-components';
import { ThemeName } from './theme';
interface Props {
themeName : ThemeName
}
export const GlobalStyle = createGlobalStyle<Props>`
body {
padding: 0;
margin: 0;
background-color: ${(props) =>(props.themeName
=== "light" ? "white" : "black")};
}
h1 {
margin: 0;
}
*{
color : ${(props) => (props.themeName === "light" ? "black" : "white")};
}
`;
// context / themeContext.tsx
import { createContext, ReactNode } from 'react';
import { ThemeName } from '../style/theme';
interface State {
themeName : ThemeName,
setThemeName : (themeName : ThemeName) => void;
}
export const state = {
themeName : "light" as ThemeName,
setThemeName: (themeName : ThemeName) => {},
};
export const ThemeContext = createContext<State>(state);
export const BookStoreThemeProvider = ({children} : {children: React.ReactNode})
=> {
return(
<ThemeContext.Provider value={state}>{children}</ThemeContext.Provider>
)
};
// App.tsx
import Layout from './components/layout/Layout';
import Home from './pages/Home';
import { GlobalStyle } from './style/global';
import { ThemeProvider } from 'styled-components';
import { getTheme } from './style/theme';
import ThemeSwitcher from './components/header/ThemeSwitcher';
import { useContext } from 'react';
import { BookStoreThemeProvider, ThemeContext } from './context/themeContext';
function App() {
const {themeName, setThemeName } = useContext(ThemeContext);
return(
<BookStoreThemeProvider>
<ThemeProvider theme={getTheme(themeName)}>
<GlobalStyle themeName={themeName} />
<ThemeSwitcher themeName={themeName} setThemeName={setThemeName} />
<Layout>
<Home />
</Layout>
</ThemeProvider>
</BookStoreThemeProvider>
/** 2번 방법
<Layout children={<Home />} />
*/
);
}
export default App;
// 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>
/** 2번 방법
<Layout children={<Home />} />
*/
);
}
export default App;
// themeContext.tsx
import { createContext, useState } from 'react';
import { getTheme, ThemeName } from '../style/theme';
import { ThemeProvider } from 'styled-components';
import { GlobalStyle } from '../style/global';
interface State {
themeName : ThemeName,
toggleTheme : () => void;
}
export const state = {
themeName : "light" as ThemeName,
toggleTheme: () => {},
};
export const ThemeContext = createContext<State>(state);
export const BookStoreThemeProvider = ({children} : {children: React.ReactNode}) => {
const [themeName, setThemeName] = useState<ThemeName>("light");
const toggleTheme = () => {
setThemeName(themeName === "light" ? "dark" : "light");
}
return(
<ThemeContext.Provider value={{themeName, toggleTheme}}>
<ThemeProvider theme={getTheme(themeName)}>
<GlobalStyle themeName={themeName} />
{children}
</ThemeProvider>
</ThemeContext.Provider>
)
};
// ThemeSwitcher.tsx
import { useContext } from 'react';
import { ThemeContext } from '../../context/themeContext';
const ThemeSwitcher = () => {
const { themeName, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>{themeName}</button>
)
}
export default ThemeSwitcher;
// context / themeContext.tsx
import { createContext, 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_LOCAL_STORAGE_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: React.ReactNode}) => {
const [themeName, setThemeName] = useState<ThemeName>(DEFAULT_THEME_NAME);
const toggleTheme = () => {
setThemeName(themeName === "light" ? "dark" : "light");
localStorage.setItem(THEME_LOCAL_STORAGE_KEY, themeName === "light" ? "dark" : "light");
}
useEffect(()=>{
const savedThemeName = localStorage.getItem(THEME_LOCAL_STORAGE_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>
)
};
🍎🍏 오늘의 느낀점 : 타입스크립트 기본 정리, Context 다시 정리, localstorage 사용법 다시 복기할 것... 그동안 클론 코딩 했던게 조금씩 여기서도 사용되었다. 타입과 같이 사용하니 너무 헷갈린다 ㅠㅠ 제네릭부터,, 타입스크립트 기본 공부해야겠다 ..ㅠㅠ