본문 바로가기
TIL with Programmers

[TIL] 11/11 React context API, 테마 스위쳐 구현, 모르는 것 정리

by 보먀 2024. 11. 11.
728x90
반응형

1. styled-components

 

리액트와 같은 자바스크립트 프레임워크에서 CSS 작성을 도와주는 CSS-in-JS 라이브러리로, 자바스크립트 코드 안에서 스타일을 작성할 수 있게 해준다. 자바스크립트 코드 안에서 스타일을 작성하기 때문에 컴포넌트 단위로 스타일을 지정하여 모듈화된 코드 작성이 가능해져 유지보수와 확장성을 높이는 데 유리하다. 

 

 

1.1. 특징

 

- CSS-in-JS: 자바스크립트 파일 안에서 직접 스타일링을 정의 -> 컴포넌트 자체적으로 스타일을 정의해 컴포넌트 간의 스타일이 격리되고 CSS 충돌을 방지할 수 있음

 

- Tagged Template Literals: 자바스크립트의 Tagged Template Literals 문법을 사용하여 CSS 를 정의하여 CSS 와 JavaScript 를 직접적으로 결합

import styled from 'styled-components';

const Button = styled.button`
  background: blue;
  color: white;
`;

 

- 동적 스타일링: 컴포넌트의 props 에 따라 스타일을 동적으로 변경할 수 있음

const Button = styled.button`
  background: ${props => props.primary ? 'blue' : 'gray'};
  color: white;
`;

// 사용시
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>

 

- CSS 코드 재사용: 스타일을 믹스하고 여러 컴포넌트에서 재사용할 수 있도록 해줌

const buttonStyles = css`
  padding: 10px 20px;
  border-radius: 5px;
`;

const Button = styled.button`
  ${buttonStyles}
  background: blue;
  color: white;
`;

const SecondaryButton = styled.button`
  ${buttonStyles}
  background: gray;
  color: white;
`;

 

- Theming 지원: ThemeProvider 를 통해 전역 스타일을 정의하는 테마 기능을 제공

import { ThemeProvider } from 'styled-components';

const theme = {
  primaryColor: 'blue',
  secondaryColor: 'gray',
};

const Button = styled.button`
  background: ${props => props.theme.primaryColor};
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
`;

// ThemeProvider로 감싸기
<ThemeProvider theme={theme}>
  <Button>Primary Button</Button>
</ThemeProvider>

 

 

※ 추가 - styled-components 의 props 타입으로는 type 별칭과 interface 만 사용가능!

 

스타일 컴포넌트에서 props 의 타입을 제네릭으로 정의할 때는 인터페이스나 타입 별칭을 사용하여야 한다. 

기본적인 타입 (boolean, string, number) 로 타입만 직접 넣게 되면 에러가 난다. 

interface ButtonProps {
  primary?: boolean;
}

const Button = styled.button<ButtonProps>`
  background: ${props => props.primary ? 'blue' : 'gray'};
  color: white;
`;

// 사용시
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>

 

 

 

2. React Context API

 

리액트의 Context API컴포넌트 트리 전체에 걸쳐 데이터를 손쉽게 전달하고, 컴포넌트 간의 상위-하위 관계 없이 상태를 공유할 수 있도록 돕는 기능이다. 이를 통해 상태나 함수를 중간 컴포넌트에 props 로 일일이 전달하지 않고도 손쉽게 공유할 수 있다.

 

컴포넌트 구조가 깊어질수록 props drilling 이 (필요한 데이터를 하위 컴포넌트에 전달하기 위해 중간 컴포넌트를 하나하나 거쳐서 props 를 전달하는 과정) 발생할 수 있는데, Context API 를 사용하면 prop drilling 을 방지할 수 있다. 

 

 

2.1. Context API 사용하기

 

- Context 생성

import React, { createContext } from 'react';

// 테마 컨텍스트에서 사용할 객체 형태를 정의
interface State {
    themeName: ThemeName,
    toggleTheme: () => void;
}

// 초기 상태 설정 및 컨텍스트 생성 -> 테마 컨텍스트에서 사용할 객체 형태를 정의
export const state: State = {
    themeName: DEFAULT_THEME_NAME,
    toggleTheme: () => {}
};

// Context 생성
export const ThemeContext = createContext<State>(state);

 

 

- provider 컴포넌트 만들기

 

createContext 로 만든 ThemeContext 는 Provider 를 제공한다. 

Provider 는 하위 컴포넌트들에게 value 를 통해 값을 전달하고, 하위 컴포넌트에서는 전달된 값을 useContext 라는 훅을 이용해 사용할 수 있다. (Provider 가 감싸고 있는 하위 컴포넌트에서는 모두 context 에 접근가능)

export const BookStoreThemeProvider = ({children}: {children: 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');
    };

    // ...

    return (
        <ThemeContext.Provider value={{themeName, toggleTheme}}>
            <ThemeProvider theme={getTheme(themeName)}>
                <GlobalStyle themeName={themeName} />
                {children}
            </ThemeProvider>
        </ThemeContext.Provider>
    );
}

 

 

- useContext 를 사용하여 Context 데이터 가져오기

 

ThemeContext 의 하위 컴포넌트읜 ThemeSwitcher 에서는 useContext 를 사용해서 Context 에 있는 데이터를 사용할 수 있다.

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

const ThemeSwitcher = () => {
    const { themeName, toggleTheme } = useContext(ThemeContext); // useContext 사용

    return (
        <div>
            <button onClick={toggleTheme}>{themeName}</button>
        </div>
  )
}

export default ThemeSwitcher

 

 

 

3. 토글 버튼으로 테마 변경 구현하기 (light/dark)

 

만들려고 하는 기능: 테마스위처

1. 사용자는 토글 UI 를 통해 웹사이트의 색상 테마를 바꿀 수 있다.

2. 색상 테마는 전역상태로 존재

3. 사용자가 선택한 테마는 로컬스토리지에 저장

 

코드 (일부 코드 생략)

// App.tsx

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

function App() {

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

export default App;
// ThemeContext.tsx

import React, { createContext, ReactNode, useEffect, useState } from 'react'
import { ThemeName } from '../style/theme';
import { ThemeProvider } from 'styled-components';
import { getTheme } from '../style/theme';
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: State = {
    themeName: DEFAULT_THEME_NAME,
    toggleTheme: () => {}
};

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

// children 을 props 로 받아 모든 하위 컴포넌트가 테마를 사용할 수 있도록 설정
export const BookStoreThemeProvider = ({children}: {children: 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');
    };

    // localstorage 에 초기값이 있다면 받아오고 없다면 디폴트 테마 적용
    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>
    );
}
import React, { useContext } from 'react'
import { ThemeContext } from '../../context/ThemeContext';

// 테마 스위처는 스스로 상태를 가지는 것이 아니라 useContext 에 의존해서 상태를 바꿈
const ThemeSwitcher = () => {
    const { themeName, toggleTheme } = useContext(ThemeContext);

    return (
        <div>
            <button onClick={toggleTheme}>{themeName}</button>
        </div>
  )
}

export default ThemeSwitcher

 

context 에는 themeName, toggleTheme 라는 값이 들어있고, ThemeContext.Provider 는 해당 값들을 자신의 children 컴포넌트들에게 넘겨준다. children 의 타입을 React.Node 로 지정했기 때문에 모든 리액트 컴포넌트들이 children 자리에 올 수 있다

 

ThemeProvider 컴포넌트는 styled-components 에서 제공하는 컴포넌트로 애플리케이션 전체에서 테마를 관리하고 적용하는 데 사용하는 컴포넌트이다. ThemeContext 에서는 어떤 테마인지에 대한 데이터를 제공한다면, ThemeProvider 는 context 에서 가져온 테마 데이터에 해당하는 데이터를 가져와서 하위 컴포넌트에 테마를 제공하는 역할을 한다. 

 

GlobalStyle 컴포넌트는 ThemeProvider 컴포넌트가 제공한 테마를 전역에서 사용할 수 있도록 해주는 역할을 하며, styled-components 에서 제공하는 createGlobalStyle 로 만들 수 있다. GlobalStyle 로 정의된 스타일은 하위 컴포넌트들에게 자동으로 상속되기 때문에 스타일이 특정 컴포넌트에 한정되지 않고 애플리케이션 전역에 걸쳐 스타일이 적용된다. 

 

ThemeSwitcher 는 스스로 상태를 가지는 것이 아니라 useContext 를 사용해서 context 를 읽어온다. 읽어온 데이터를 통해서 테마를 바꾸어준다. 

 

 

궁금한 점1. Context 에서 사용할 객체에서 상태 변경 함수는 왜 빈 함수?

 

-> 컴포넌트가 렌더링되면서 자동으로 toggleTheme 는 덮어씌워짐, 상태 변경 함수가 아직 정의되지 않았거나 Provider 로 해당 값이 전달되지 않았을 경우에도 Context 를 사용할 컴포넌트들이 정상적으로 동작할 수 있도록 일단 빈 함수라도 만들어 놓는 것.

 

 

궁금한 점2.

- 자동으로 덮어씌워 진다면 왜 굳이 컴포넌트 밖에서 Context 에서 사용할 객체를 만드는지?

- Context 에서 사용할 객체는 원래 컴포넌트 밖에서 만드는건지?

 

-> 일반적으로 Context 는 리액트의 최적화와 컴포넌트 재렌더링을 효율적으로 관리하기 위해 컴포넌트 바깥에서 정의하는 것이 권장됨. 

 

Context 를 컴포넌트 안에서 정의하게 되면, 컴포넌트가 렌더링될 때마다 새로운 Context 객체가 생성됨 -> 리액트가 전과 동일한 Context 가 아닌 것으로 인식 -> 불필요하게 하위 컴포넌트들이 재렌더링될 수 있음

 

 

 

4. 타입 가드 (Type Guard)

타입 스크립트에서는 변수나 값의 타입을 좁혀서(더욱 구체적인 정보제공) 타입 정보를 제공하여 안정성을 높일 수 있다.

export type ThemeName = "light" | "dark";
type ColorKey = "primary" | "background" | "secondary" | "third"; // 컬러가 계속해서 늘어날 수 있기 때문에 

interface Theme {
    name: ThemeName,
    color: {
        [key in ColorKey]: string;
    }
}


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

 

(나는 자바스크립트, 파이썬 같은 언어만 써봐서 타입에 익숙하지 않은데, 생각보다 더 빡빡하게 타입을 좁혀놓는 것이 신기하면서 어려웠다. 확실히 타입을 좁혀놓으니 빡빡하긴 해도 실행 전에 오류를 잡기 쉬워서 안정성이 올라갈 수 밖에 없겠다고 느껴졌다. )

 

 

5. 자잘자잘 알게된 것

 

5.1. 리액트 컴포넌트의 children 을 전달하는 두 가지 방법

 

기본적인 방법으로 컴포넌트 안에 자식을 직접적으로 포함시키는 방법

<Layout><Home /></Layout>

 

children 을 props 로 전달하는 방법

<Layout children={<Home />} />

 

 

5.2. 객체의 속성 타입 선언

export const BookStoreThemeProvider = ({ children }: { children: ReactNode }) => {
  // ...
}

 

{ children } : ReactNode 라고 쓰면 되는거 아닌가? 라고 생각했는데, children 은 객체의 속성이기 때문에 { children: ReactNode } 로 써야한다.

 

728x90
반응형