๐ŸŽฏ @vanilla-extract/css ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ redux๋ฅผ ํ™œ์šฉํ•˜์—ฌ Trello ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ“— Today I Learned

Trello ํ™”๋ฉด

  • ์ƒ๋‹จ: ํ˜„์žฌ ์„ ํƒ๋œ Board์˜ List ๋ชฉ๋ก์ด ๊ฐ€๋กœ๋กœ ๋‚˜์—ด๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ๋ฆฌ์ŠคํŠธ์—๋Š” Task ์นด๋“œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉฐ, ๋ฆฌ์ŠคํŠธ์™€ ์นด๋“œ์˜ ์ถ”๊ฐ€ ๋ฐ ์ˆ˜์ •์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

    • ๊ฒŒ์‹œํŒ ์ถ”๊ฐ€: ์˜ค๋ฅธ์ชฝ ๋งจ ์œ„์— ์žˆ๋Š” ๋ฒ„ํŠผ+์„ ํด๋ฆญํ•˜๋ฉด ์ƒˆ๋กœ์šด ๋ณด๋“œ๊ฐ€ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค.

    • ๋ฆฌ์ŠคํŠธ ์ถ”๊ฐ€: + ์ƒˆ๋กœ์šด ๋ฆฌ์ŠคํŠธ ๋“ฑ๋ก ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์ƒˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    • ์ž‘์—… ์ถ”๊ฐ€: ๋ฆฌ์ŠคํŠธ ์•ˆ์˜ + ์ƒˆ๋กœ์šด ์ผ ๋“ฑ๋ก ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด Task๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ํ•˜๋‹จ: ์ด ๊ฒŒ์‹œํŒ ์‚ญ์ œํ•˜๊ธฐ ๋ฒ„ํŠผ๊ณผ ํ™œ๋™ ๋ชฉ๋ก ๋ณด์ด๊ธฐ ๋ฒ„ํŠผ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

    • ๋ณด๋“œ ์‚ญ์ œ: ํ˜„์žฌ ์„ ํƒ๋œ Board๋ฅผ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (์ตœ์†Œ 1๊ฐœ์˜ ๊ฒŒ์‹œํŒ์€ ์œ ์ง€)

    • ํ™œ๋™ ๋กœ๊ทธ ํ™•์ธ: ์‚ญ์ œ, ์ถ”๊ฐ€, ์ˆ˜์ • ๋“ฑ์˜ ๋กœ๊ทธ๋“ค์ด ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค.




App.tsx

// React ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ useState ์ž„ํฌํŠธ
import { useState } from 'react';

// CSS ๋ชจ๋“ˆ ์Šคํƒ€์ผ๋“ค ์ž„ํฌํŠธ
import {
  appContainer,
  board,
  buttons,
  deleteBoardButton,
  loggerButton,
} from './App.css';

// ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๋“ค ์ž„ํฌํŠธ
import BoardList from './components/BoardList/BoardList';
import ListsContainer from './components/ListsContainer/ListsContainer';
import ModalEdit from './components/ModalEdit/ModalEdit';
import LoggerModal from './components/LoggerModal/LoggerModal';

// ์ปค์Šคํ…€ ํ›…: ํƒ€์ž…์ด ์ง€์ •๋œ Redux ๋””์ŠคํŒจ์น˜ & ์…€๋ ‰ํ„ฐ ํ›…
import { useTypedDispatch, useTypedSelector } from './hooks/redux';

// Redux ์Šฌ๋ผ์ด์Šค์—์„œ ์•ก์…˜ ๊ฐ€์ ธ์˜ค๊ธฐ
import { deleteBoard } from './store/slices/boardsSlice';
import { addLog } from './store/slices/loggerSlice';

// ๋กœ๊ทธ ID ์ƒ์„ฑ์„ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
import { v4 } from 'uuid';

function App() {
  // Redux ์•ก์…˜ ์‹คํ–‰์šฉ
  const dispatch = useTypedDispatch();
  
   // ์ง€์—ญ ์ƒํƒœ
  const [isLoggerOpen, setIsLoggerOpen] = useState(false);
  const [activeBoardId, setActiveBoardId] = useState('board-0');
  
  // ์ „์—ญ ์ƒํƒœ
  const modalActive = useTypedSelector((state) => state.boards.modalActive);
  const boards = useTypedSelector((state) => state.boards.boardArray);

  // ํ˜„์žฌ ํ™œ์„ฑ ๋ณด๋“œ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
  const getActiveBoard = boards.filter(
    (board) => board.boardId === activeBoardId
  )[0];
 // ํ™œ์„ฑ ๋ณด๋“œ์˜ ๋ฆฌ์ŠคํŠธ๋“ค ๊ฐ€์ ธ์˜ค๊ธฐ
  const lists = getActiveBoard.lists;

  // ๊ฒŒ์‹œํŒ ์‚ญ์ œ ํ•จ์ˆ˜
  const handleDeleteBoard = () => {
    if (boards.length > 1) {
      // Redux์— ์‚ญ์ œ ์•ก์…˜ ๋””์ŠคํŒจ์น˜
      dispatch(
        deleteBoard({
          boardId: getActiveBoard.boardId,
        })
      );
      
      // Redux์— ๋กœ๊ทธ ์ถ”๊ฐ€ ์•ก์…˜ ๋””์ŠคํŒจ์น˜
      dispatch(
        addLog({
          logId: v4(),
          logMessage: `๊ฒŒ์‹œํŒ ์ง€์šฐ๊ธฐ : ${getActiveBoard.boardName}`,
          logAuthor: 'User',
          logTimestamp: String(Date.now()),
        })
      );

      // ์‚ญ์ œ ํ›„ ์–ด๋–ค ๊ฒŒ์‹œํŒ์„ ๋ณด์—ฌ์ค„์ง€ ๊ฒฐ์ •
      const newIndexToSet = () => {
        const indexToBeDeleted = boards.findIndex(
          (board) => board.boardId === activeBoardId
        );
        return indexToBeDeleted === 0
          ? indexToBeDeleted + 1
          : indexToBeDeleted - 1;
      };

      // ์ƒˆ๋กœ์šด ๋ณด๋“œ ID๋กœ ์ „ํ™˜
      setActiveBoardId(boards[newIndexToSet()].boardId);
    } else {
      alert('์ตœ์†Œ ๊ฒŒ์‹œํŒ ๊ฐœ์ˆ˜๋Š” ํ•œ ๊ฐœ์ž…๋‹ˆ๋‹ค.');
    }
  };

  return (
    <div className={appContainer}>
      {isLoggerOpen && <LoggerModal setIsLoggerOpen={setIsLoggerOpen} />}
      {modalActive && <ModalEdit />}

      <BoardList
        activeBoardId={activeBoardId}
        setActiveBoardId={setActiveBoardId}
      />
      <div className={board}>
        <ListsContainer lists={lists} boardId={getActiveBoard.boardId} />
      </div>
      <div className={buttons}>
        <button className={deleteBoardButton} onClick={handleDeleteBoard}>
          ์ด ๊ฒŒ์‹œํŒ ์‚ญ์ œํ•˜๊ธฐ
        </button>
        <button
          className={loggerButton}
          onClick={() => setIsLoggerOpen(!isLoggerOpen)}
        >
          {isLoggerOpen ? 'ํ™œ๋™ ๋ชฉ๋ก ์ˆจ๊ธฐ๊ธฐ' : 'ํ™œ๋™ ๋ชฉ๋ก ๋ณด์ด๊ธฐ'}
        </button>
      </div>
    </div>
  );
}

export default App;

์ƒํƒœ ๊ด€๋ฆฌ ๊ธฐ์ค€: useState vs Redux

  • ์ง€์—ญ ์ƒํƒœ

    • isLoggerOpen, activeBoardId : App ์ปดํฌ๋„ŒํŠธ์—์„œ๋งŒ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— useState๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ด€๋ฆฌํ•ด์ค๋‹ˆ๋‹ค.
  • ์ „์—ญ ์ƒํƒœ

    • modalActive, boards : ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์—์„œ ํ•„์š”ํ•œ ๊ตฌ์กฐ์ด๊ธฐ ๋•Œ๋ฌธ์— Redux๋ฅผ ํ™œ์šฉํ•ด์„œ ๊ด€๋ฆฌํ•ด์ค๋‹ˆ๋‹ค.

๊ฒŒ์‹œํŒ ์‚ญ์ œ ํ•จ์ˆ˜( handleDeleteBoard )

  • handleDeleteBoard : ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ๊ฒŒ์‹œํŒ์„ ์‚ญ์ œํ•˜๊ณ , ๋กœ๊ทธ๋ฅผ ๊ธฐ๋กํ•˜๋ฉฐ, ์‚ญ์ œ ํ›„ ๋ณด์—ฌ์ค„ ๊ฒŒ์‹œํŒ์„ ์ƒˆ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

    • newIndexToSet: ์‚ญ์ œ ๋Œ€์ƒ์ด ์ฒซ ๋ฒˆ์งธ ๋ณด๋“œ๋ฉด ๋‹ค์Œ ๋ณด๋“œ ๋ณด์—ฌ์ฃผ๊ณ , ์•„๋‹ˆ๋ฉด ์ด์ „ ๋ณด๋“œ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.



App.css.ts

import { createGlobalTheme, style } from '@vanilla-extract/css';

export const vars = createGlobalTheme(':root', {
  color: {
    main: '#ffa726',
    mainDarker: '#f57c00',
    mainFaded: '#ffb74d',
    mainFadedBright: '#ffb74da6',
    list: 'rgb(235,236,240)',
    task: 'rgb(255,255,255)',
    taskHover: 'rgb(245,245,245)',
    brightText: 'rgb(255,255,255)',
    darkText: 'rgb(24,42,77)',
    secondaryDarkText: 'rgb(94,108,132)',
    secondaryDarkTextHover: 'rgb(218,219,226)',
    selectedTab: 'rgb(137,176,74)',
    updateButton: 'rgb(237,180,88)',
    deleteButton: 'rgb(237,51,88)',
  },
  fontSizing: {
    T1: '32px',
    T2: '24px',
    T3: '18px',
    T4: '14px',
    P1: '12px',
  },
  spacing: {
    small: '5px',
    medium: '10px',
    big1: '20px',
    big2: '15px',
    listSpacing: '30px',
  },
  font: {
    body: 'arial',
  },
  shadow: {
    basic: '4px 4px 8px 0px rgba(34, 60, 80, 0.2)',
  },
  minWidth: {
    list: '250px',
  },
});

export const appContainer = style({
  display: 'flex',
  flexDirection: 'column',
  minHeight: '100vh',
  height: 'max-content',
  width: '100vw',
});

export const board = style({
  display: 'flex',
  flexDirection: 'row',
  height: '100%',
});

export const buttons = style({
  marginTop: 'auto',
  paddingLeft: vars.spacing.big2,
});

export const deleteBoardButton = style({
  border: 'none',
  borderRadius: 5,
  width: 'max-content',
  marginTop: 'auto',
  marginLeft: 'auto',
  marginBottom: '30px',
  fontSize: vars.fontSizing.T4,
  padding: vars.spacing.big2,
  backgroundColor: vars.color.mainFaded,
  cursor: 'pointer',
  opacity: 0.6,
  minWidth: 150,
  ':hover': {
    opacity: 0.8,
  },
});

export const loggerButton = style({
  border: 'none',
  borderRadius: 5,
  width: 'max-content',
  marginTop: 'auto',
  marginLeft: '15px',
  marginRight: '30px',
  marginBottom: '30px',
  fontSize: vars.fontSizing.T4,
  padding: vars.spacing.big2,
  backgroundColor: vars.color.mainFaded,
  cursor: 'pointer',
  opacity: 0.6,
  minWidth: 150,
  ':hover': {
    opacity: 0.8,
  },
});

@vanilla-extract/css ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด ์ „์—ญ ํ…Œ๋งˆ( createGlobalTheme )์™€ ๊ฐ ์ปดํฌ๋„ŒํŠธ์˜ ์Šคํƒ€์ผ( style )์„ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

  • createGlobalTheme : ์ „์—ญ ํ…Œ๋งˆ ๋ณ€์ˆ˜๋ฅผ ์„ ์–ธํ•จ์œผ๋กœ์จ vars.color.main์˜ ๋ฐฉ์‹์œผ๋กœ ์žฌ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.



โœ๏ธ ํšŒ๊ณ 

์ „์—ญ ํ…Œ๋งˆ ์„ค์ • ๋•๋ถ„์— ๋””์ž์ธ์„ ์‰ฝ๊ฒŒํ•  ์ˆ˜ ์žˆ์–ด์„œ ์ข‹์€ ๊ฑฐ ๊ฐ™๊ณ , useState์™€ Redux ์‚ฌ์ด์—์„œ ์–ด๋–ค ๊ฑด ๋ญ˜ ์จ์•ผ ํ•˜๋Š”์ง€ ๋” ๊ณ ๋ฏผํ•ด๋ณด๋ฉด์„œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค.

profile
๐ŸŒฑ๊ฐœ๋ฐœ ๊ธฐ๋ก์žฅ

0๊ฐœ์˜ ๋Œ“๊ธ€