Front-End/React

๋‹คํฌ๋ชจ๋“œ ๊ตฌํ˜„ ํ•˜๊ธฐ (react, reduxToolkit, styled components)

  • -

๐Ÿ“ ์‹œ์ž‘

์˜ˆ์ „๋ถ€ํ„ฐ ๋‹คํฌ ๋ชจ๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์‹ถ์—ˆ๋‹ค.
์ด๋ฒˆ ๊ธฐํšŒ์— ๊ณต๋ถ€ํ•  ์ˆ˜ ์žˆ์—ˆ๋˜ ๊ณ„๊ธฐ๊ฐ€ ๋˜์—ˆ๋‹ค.

์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ์—ˆ์ง€๋งŒ body์— ํ…Œ๋งˆ ์Šคํƒ€์ผ์„ ์ง€์ •ํ•˜๊ณ 
<ThemeProvider>์™€ theme์†์„ฑ์„ ํ™œ์šฉํ•˜๋Š” ๋Œ€์ค‘์ ์ธ ๋ฐฉ๋ฒ•์„ ์„ ํƒํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

ํ† ๊ธ€ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํ…Œ๋งˆ๊ฐ€ ๋ณ€๊ฒฝ๋˜๊ณ 
์ƒˆ๋กœ๊ณ ์นจ ํ•ด๋„ ์œ ์ง€๋˜์–ด์•ผ ํ•˜๊ธฐ์— localStorage๋„ ๊ฐ™์ด ํ™œ์šฉํ–ˆ๋‹ค.


๐Ÿฅน ๋ฌธ์ œ

์ „์—ญ์œผ๋กœ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ• ๊นŒ ํ–ˆ์ง€๋งŒ
localStorage์— ์ €์žฅํ•˜๊ธฐ์— ํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค ์ƒ๊ฐํ–ˆ๋‹ค.

const themeMode = localStorage.getItem('THEME');

const handlerTheme = () => {
  if (themeMode === 'dark') {
    localStorage.setItem('THEME', 'light');
  } else {
    localStorage.setItem('THEME', 'dark');
  }
};
function MyApp() {
  const themeMode = locaStorage.getItem('THEME');
  return (
    <ThemeProvider theme={themeMode === 'dark' ? dark : light}>
      <GlobalStyle />
      <App />
    </ThemeProvider>
  );
}

์šฐ์„ ์€ hook ๋“ฑ์„ ๋ฐ”๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๋‹ค์ด๋ ‰ํŠธ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด ๋ณด์•˜๋‹ค.
๊ด€๋ฆฌ์ž ๋ชจ๋“œ๋กœ localStorage๋ฅผ ํ™•์ธํ•˜๋ฉด ํ† ๊ธ€ ๊ธฐ๋Šฅ์€ ์ž˜ ์ž‘๋™ํ–ˆ๋‹ค.

ํ•˜์ง€๋งŒ ํ…Œ๋งˆ ์Šคํƒ€์ผ์ด ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.
(์—ญ์‹œ ์„ธ์ƒ์€ ํ˜ธ๋ฝํ˜ธ๋ฝํ•˜์ง€ ์•Š๋‹ค..๐Ÿฅน)

๋‚ด๊ฐ€ ์ƒ๊ฐํ–ˆ์„ ๋•Œ์˜ ๋ฌธ์ œ๋Š”
localStorage์˜ ๊ฐ’์„ ๊ฐ์ง€ํ•˜๊ณ  ๋ฆฌ๋ Œ๋”๋งํ•ด์•ผ ํ•˜๋Š”๋ฐ ๊ทธ๋Ÿฌ์ง€ ๋ชปํ•˜๋Š” ๊ฒƒ ๊ฐ™๋‹ค.

๋˜ํ•œ Redux Toolkit๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉํ•ด ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์–ด
Redux Toolkit์œผ๋กœ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  localStorage๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

โœจ ๊ตฌํ˜„

๊ทธ๋Ÿฌ๋‹ค ํ•ด๋‹น ๊ฒŒ์‹œ๋ฌผ์„ ์ฐธ๊ณ ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

์ฐธ๊ณ  : https://velog.io/@velopert/velog-dark-mode

์ฐธ๊ณ  ๊ฒŒ์‹œ๋ฌผ์—์„œ๋Š” ๋ธŒ๋ผ์šฐ์ € ์‹œ์Šคํ…œ ์„ธํŒ…๋„ ๊ณ ๋ คํ•˜์‹  ๋ชจ์–‘์ด๋‹ค.
์ด๋ถ€๋ถ„์€ ์ดํ›„ ๋ฆฌํŒฉํ† ๋ง ๋‹จ๊ณ„์—์„œ ์‹œ๋„ํ•ด ๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค.
์ถ”ํ›„ ๋‹ค์‹œ ์ •๋…ํ•˜๊ณ  ์ฝ”๋“œ๋ฅผ ๋ฆฌํŒฉํ„ฐ๋ง ํ•ด๋ด์•ผ๊ฒ ๋‹ค.

0. style ์„ค์ •

//styles/theme.ts
export const light = {
  colors: {
    bgColor: '#fff',
    boxColor: '#fff',
    textColor: '#000',
    iconColor: '#414443',
    iconBoxColor: '#f1f1f1',
  },
};

export const dark = {
  colors: {
    bgColor: '#222',
    boxColor: '#444',
    textColor: '#FDFDFD',
    iconColor: '#FDFDFD',
    iconBoxColor: '#444',
  },
};

light, dark ๋‘˜ ๋‹ค ํ‚ค ๊ฐ’ ์ด๋ฆ„์ด ๋™์ผํ•ด์•ผ ํ•œ๋‹ค.

// styles/GlobalStyles
:root {
    --bg-color: ${(props) => props.theme.colors.bgColor};
    --box-color: ${(props) => props.theme.colors.boxColor};
    --text-color: ${(props) => props.theme.colors.textColor};
    --icon-color: ${(props) => props.theme.colors.iconColor};
    --icon-box-color: ${(props) => props.theme.colors.iconBoxColor};
  }
.
.
.
body{ 
  background-color: var(--bg-color);
}

css ํ˜•์‹์œผ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ํŽธํ•˜๊ฒŒ ํ–ˆ๋‹ค.
์ด์ œ styled components์— ์‚ฌ์šฉํ•  ๋•Œ css๋ฐฉ์‹์ฒ˜๋Ÿผ ์ž‘์„ฑํ•ด ์ฃผ๋ฉด ๋œ๋‹ค.

const TextColor = css`
  color: var(--text-color);
`;

1. slice ์„ธํŒ…

// slice.ts
import { createSlice } from '@reduxjs/toolkit';

type initialStateT = {
  themeMode: string;
};
const toggleThemeMode = localStorage.getItem('THEME');

const initialState: initialStateT = {
  themeMode: toggleThemeMode ? toggleThemeMode : 'light',
};

const themeSlice = createSlice({
  name: 'themetype',
  initialState,
  reducers: {
    toggleDarkMode(state) {
      state.themeMode = 'dark';
    },
    toggleLightMode(state) {
      state.themeMode = 'light';
    },
  },
});

export const themeReducer = themeSlice.reducer;
export const themeActions = themeSlice.actions;

๋‹คํฌ๋ชจ๋“œ, ๋ผ์ดํŠธ๋ชจ๋“œ๋ฅผ ์ „์—ญ์—์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ๋‹ค.

const initialState: initialStateT = {
  themeMode: toggleThemeMode ? toggleThemeMode : 'light',
};

๋˜ํ•œ themeMode๋Š” localStorage์— ๊ฐ’์ด ์žˆ์„ ๊ฒฝ์šฐ์—๋งŒ ํ• ๋‹น๋˜๊ณ  ์—†์„ ๊ฒฝ์šฐ์—” ๊ธฐ๋ณธ์ ์œผ๋กœ light ํ…Œ๋งˆ๋ฅผ ์ œ๊ณตํ•˜๋„๋ก ํ–ˆ๋‹ค.

2. ํ† ๊ธ€ ๋ฒ„ํŠผ ์ž‘์—…

const Nav = () => {
  const dispatch = useDispatch();
  const themeMode = useSelector((state: RootState) => state.themeType.themeMode);

  const themeSave = (value: 'light' | 'dark') => {
    localStorage.setItem('THEME', value);
  };

  const handlerTheme = () => {
    if (!themeMode) return;
    if (themeMode === 'dark') {
      dispatch(themeActions.toggleLightMode());
      themeSave('light');
    } else {
      dispatch(themeActions.toggleDarkMode());
      themeSave('dark');
    }
  };

  return (
    <NavContainer>
      <MenuList>
        <li>
          <SearchBtn />
        </li>
        <li>
          <ThemeBtn handlerTheme={handlerTheme} />
        </li>
      </MenuList>
    </NavContainer>
  );
};

์ฒ˜์Œ์—๋Š” ThemeBtn์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์— ์ž‘์„ฑํ• ๊นŒ ํ–ˆ์ง€๋งŒ
์žฌ์‚ฌ์šฉ์„ฑ์„ ๊ณ ๋ คํ–ˆ๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ ์‰ฝ๋„๋ก Nav ์ปดํฌ๋„ŒํŠธ์— ์ž‘์„ฑํ–ˆ๋‹ค.
(์ถ”ํ›„ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ์ž‘์—…ํ•  ์˜ˆ์ •์ด๋‹ค.)

handlerTheme ํ•จ์ˆ˜์—์„œ themeMode์˜ ๊ฐ’์ด 'dark' ๋˜๋Š” 'light'์ธ์ง€ ์กฐ๊ฑด๋ฌธ์„ ์ž‘์„ฑํ•ด
๊ฐ’์„ ํ• ๋‹นํ–ˆ๊ณ ,
themeSave ํ•จ์ˆ˜์— ์ธ์ž๋กœ ์ „๋‹ฌํ•ด localStorage์˜ ๊ฐ’๋„ ๋ณ€๊ฒฝํ–ˆ๋‹ค.

3. ThemeProvider ํ…Œ๋งˆ ์—ฐ๊ฒฐํ•˜๊ธฐ

function MyApp() {
  const themeMode = useSelector((state: RootState) => state.themeType.themeMode);
  return (
    <ThemeProvider theme={themeMode === 'dark' ? dark : light}>
      <GlobalStyle />
      <App />
    </ThemeProvider>
  );
}

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <Provider store={store}>
        <MyApp />
      </Provider>
    </QueryClientProvider>
  </React.StrictMode>,
);

์ „์—ญ์— ThemeProvider๋ฅผ ๊ฐ์‹ธ์ฃผ๊ณ  theme์†์„ฑ์„ ์ถ”๊ฐ€ํ•ด ์ค€๋‹ค.
useSelector๋ฅผ ์‚ฌ์šฉํ•ด ์ƒํƒœ ๊ฐ’์„ ๊ฐ€์ ธ์˜จ๋‹ค.

์™„์„ฑ!


๋ฒ„ํŠผ ํด๋ฆญ ์‹œ localStorage์— ๊ฐ’์ด ์ €์žฅ๋˜๊ณ  ๊ฐ’์— ๋”ฐ๋ผ ํ…Œ๋งˆ๊ฐ€ ๋ณ€๊ฒฝ๋œ๋‹ค.
localStorage์— ์ €์žฅ๋˜๊ธฐ์— ์ƒˆ ๋กœ๊ณ ์นจํ•ด๋„ ๊ฐ’์ด ์œ ์ง€๋œ๋‹ค.
์ด์ œ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ๋งŒ๋“ค์–ด ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์—ฌ ๋ณด์ž!

๐ŸŒฑ ๋ฒˆ์™ธ

์ปค์Šคํ…€ ํ›…์œผ๋กœ ๋งŒ๋“ค๊ธฐ

// hooks/useToggleTheme.ts
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '../store';
import { themeActions } from '../store/theme-slice';

const useToggleTheme = () => {
  const dispatch = useDispatch();
  const themeMode = useSelector((state: RootState) => state.themeType.themeMode);

  const themeSave = (value: 'light' | 'dark') => {
    localStorage.setItem('THEME', value);
  };

  const handlerTheme = () => {
    if (!themeMode) return;
    if (themeMode === 'dark') {
      dispatch(themeActions.toggleLightMode());
      themeSave('light');
    } else {
      dispatch(themeActions.toggleDarkMode());
      themeSave('dark');
    }
  };

  return handlerTheme;
};

export default useToggleTheme;

์ปค์Šคํ…€ ํ›…์„ ๋งŒ๋“ค ๋•Œ React์—์„œ ์ œ๊ณตํ•˜๋Š” ๋‹ค๋ฅธ ํ›…๋“ค์ฒ˜๋Ÿผ
์ด๋ฆ„ ์•ž์— use๋ฅผ ๋ถ™์—ฌ ์ค˜์•ผ ํ•œ๋‹ค.

์ปค์Šคํ…€ ํ›…์„ ๋งŒ๋“ค๊ณ  handlerThemeํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ด ์ค€๋‹ค.
๋ฐ˜ํ™˜ํ•  ๊ฐ’์ด ์—ฌ๋Ÿฌ ๊ฐœ ๋ผ๋ฉด ๋ฐฐ์—ด ํ˜•์‹ [...,...,...]์œผ๋กœ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๋œ๋‹ค.

const Nav = () => {
  const handlerTheme = useToggleTheme();
  return (
    <NavContainer>
      <MenuList>
        <li>
          <SearchBtn />
        </li>
        <li>
          <ThemeBtn handlerTheme={handlerTheme} />
        </li>
      </MenuList>
    </NavContainer>
  );
};

์ด์ „์˜ ์ฝ”๋“œ๋ฅผ ์ง€์šฐ๊ณ  const handlerTheme = useToggleTheme();๋งŒ ์ž‘์„ฑํ•ด ์ฝ”๋“œ๊ฐ€ ๊น”๋”ํ•ด์กŒ๋‹ค.


โœ๏ธ ๋งˆ์น˜๋ฉฐ

์‚ฌ์‹ค ์•„์ง ๋ฏธํกํ•œ ๋ถ€๋ถ„์ด ๋งŽ๋‹ค.
๊ณต๋ถ€ํ•˜๋Š” ์ฐจ์›์— ์ง„ํ–‰ํ•ด ๋ดค๋Š”๋ฐ ํ•˜๋‚˜ํ•˜๋‚˜ ๊ตฌํ˜„ํ•ด ๋ณด๋Š” ์พŒ๊ฐ์ด ๋“ค์—ˆ๋‹ค ใ…Žใ…Ž
์‚ฌ์šฉ์ž ๊ฐœ์ธ์ด ์„ธํŒ…ํ•œ ํ…Œ๋งˆ ์„ค์ • ๋“ฑ์€ ์ž‘์—…์„ ์•ˆ ํ•ด์„œ ์ถ”ํ›„ ๋กœ์ง ์ˆ˜์ •์ด ํ•„์š”ํ•˜์ง€๋งŒ
๊ตฌํ˜„ํ–ˆ๋‹ค๋Š” ์ ์— ์˜๋ฏธ๋ฅผ ๋‘๋ ค๊ณ  ํ•œ๋‹ค.

ํ•ด๋‹น ํ† ์ด ํ”„๋กœ์ ํŠธ๊ฐ€ ๋งˆ๋ฌด๋ฆฌ๋˜๋ฉด ๋ฆฌํŒฉํ† ๋ง ์ฐจ์›์—์„œ ์ง„ํ–‰ํ•ด ๋ณด๋ ค๊ณ  ํ•œ๋‹ค.

์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์ด ์กด์žฌํ•˜๊ณ  ๋‚ด๊ฐ€ ์ฐธ๊ณ ํ•œ ๊ฒŒ์‹œ๋ฌผ๋„ css๋ฅผ ์ด์šฉํ•ด ์ง„ํ–‰ํ•˜์…จ๋‹ค.

์›๋ž˜๋Š” ๋ฒจ๋กœ๊ทธ์— ๋ธ”๋กœ๊ทธ๋ฅผ ์ž‘์„ฑํ–ˆ์ง€๋งŒ...
๊ฐ„ํ˜น ์ž๋™์œผ๋กœ ๋น„๊ณต๊ฐœ ์ฒ˜๋ฆฌ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค..ใ… 
์šฐ์„  ํ‹ฐ์Šคํ† ๋ฆฌ์— ์—…๋กœ๋“œ๋„ ๊ฐ™์ด ํ•˜๋ ค๊ณ  ํ•œ๋‹ค.

Contents

ํฌ์ŠคํŒ… ์ฃผ์†Œ๋ฅผ ๋ณต์‚ฌ์Šต๋‹ˆ๋‹ค :)

์ด ๊ธ€์ด ๋„์›€์ด ๋˜์—ˆ๋‹ค๋ฉด ๊ณต๊ฐ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค :)