[프로그래머스] 프론트엔드 심화: 프로젝트(2)

Lina Hongbi Ko·2024년 11월 11일
0

Programmers_BootCamp

목록 보기
53/76
post-thumbnail

2024년 11월 11일

✏️ 레이아웃 구성

  • 레이아웃 왜 필요할까

    • 프로젝트의 기본적인 화면 구조를 잡는다
    • 반복적으로 들어가야하는 헤더, 푸터 등을 매 화면마다 제공한다.
    • 상황과 필요에 따라 레이아웃이 변경될 수 있도록 대비한다.
    • 웹의 확장성을 위해 필요함
  • 레이아웃 컴포넌트 만들기

    • 매번 header나 footer 컴포넌트를 가져다 쓰면 귀찮음
      • 유지보수 측면에도 좋지 않음
// 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
  • ReactNode
// 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의 업그레이드 버전) → 사용 예정

        • npm i santize.css —save
        // 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)

    • 설치

      • npm install styled-components —save
    • 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>
);

✏️ 테마 만들기

  • 테마를 사용하는 이유

    • UI, UX의 일관성 유지 → 프로젝트에서 일관된 테마를 사용해야 길을 잃지 않음
    • 유지보수 용이 → 테마 추가, 변경을 했을때 편리함
    • 확장성
    • 재사용성
    • 사용자 정의 → 라이트모드/다크모드 등 기능을 위해 사용
  • 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")};
  }
`;

✏️ 테마 스위처 (1)

  • 기능과 목적

    • 사용자는 토글 UI를 통해 웹사이트의 색상 테마를 바꿀 수 있다.
    • 색상 테마는 전역 상태로 존재한다.
    • 사용자가 선택한 테마는 로컬스토리지에 저장한다.
  • 테마 토글 스위치(지역 상태 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")};
  }
`;
  • 테마 토글 스위치(app.tsx의 themename을 context[전역상태]가 state 담당하도록 수정) → 하위 컴포넌트 어디에서 쓸 수 있도록 수정 (일단 틀 만들기)
// 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;

✏️ 테마 스위처 (2)

  • context로 theme 설정 → themeswitcher 에서 state 가져와서 사용하도록 만들기 (진정한 전역으로 만들기)
// 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;
  • state를 로컬 스토리지에 저장하기(새로고침 해도 테마 유지)
// 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 사용법 다시 복기할 것... 그동안 클론 코딩 했던게 조금씩 여기서도 사용되었다. 타입과 같이 사용하니 너무 헷갈린다 ㅠㅠ 제네릭부터,, 타입스크립트 기본 공부해야겠다 ..ㅠㅠ

profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글