๐ŸŽฏ Component๋ฅผ ์ƒ์„ฑํ•˜๋ฉด์„œ @reduxjs/toolkit, react-redux ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ตํž™๋‹ˆ๋‹ค.


๐Ÿ“— Today I Learned

redux ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

Trello ๋ณด๋“œ ์ถ”๊ฐ€ UI๋ฅผ ๋งŒ๋“ค๋ฉด์„œ, Redux Toolkit์˜ createSlice, configureStore, ๊ทธ๋ฆฌ๊ณ  React-Redux์˜ useDispatch, useSelector ์‚ฌ์šฉ๋ฒ•์„ ์ตํž™๋‹ˆ๋‹ค.

1. ์ดˆ๊ธฐ ์ƒํƒœ ์ •์˜ ( types.ts )

๋ณด๋“œ, ๋ฆฌ์ŠคํŠธ, ํƒœ์Šคํฌ์˜ ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

// types.ts
export type Task = {
  taskId: string;
  taskName: string;
};

export type List = {
  listId: string;
  listName: string;
  tasks: Task[];
};

export type Board = {
  boardId: string;
  boardName: string;
  lists: List[];
};

export type BoardState = {
  boards: Board[];
};



2. slice ์ œ์ž‘ ( boardSlice.ts )

  • createSlice๋Š” ์ƒํƒœ์™€ ์ƒํƒœ๋ฅผ ๋ฐ”๊พธ๋Š” ํ•จ์ˆ˜(reducer)๋ฅผ ํ•œ ๋ฒˆ์— ๋งŒ๋“ค์–ด์ฃผ๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

  • PayloadAction<T>์œผ๋กœ ์•ก์…˜์— ๋“ค์–ด์˜ค๋Š” ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// boardSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { IBoard } from '../../types';

type TBoardState = {
  modalActive: boolean;
  boardArray: IBoard[];
};

type TAddBoardAction = {
  board: IBoard;
};

type TDeleteListAction = {
  boardId: string;
  listId: string;
};

const initialState: TBoardState = {
  modalActive: false,
  boardArray: [
    {
      boardId: 'board-0',
      boardName: '์ฒซ ๋ฒˆ์งธ ๊ฒŒ์‹œ๋ฌผ',
      lists: [
        {
          listId: 'list-0',
          listName: 'list 1',
          tasks: [
            {
              taskId: 'task-0',
              taskName: 'Task 1',
              taskDescription: 'Description',
              taskOwner: 'John',
            },
            {
              taskId: 'task-1',
              taskName: 'Task 2',
              taskDescription: 'Description',
              taskOwner: 'John',
            },
          ],
        },
        {
          listId: 'list-1',
          listName: 'list 2',
          tasks: [
            {
              taskId: 'task-3',
              taskName: 'Task 3',
              taskDescription: 'Description',
              taskOwner: 'John',
            },
          ],
        },
      ],
    },
  ],
};

const boardSlice = createSlice({
  name: 'boards',
  initialState,
  reducers: {
    addBoard: (state, { payload }: PayloadAction<TAddBoardAction>) => {
      state.boardArray.push(payload.board);
    },
  },
});

export const { addBoard } = boardSlice.actions;
export const boardsReducer = boardSlice.reducer;




3. store ์„ค์ • ( store/index.ts )

๋ฆฌ๋“€์„œ ์„ค์ •

  • reducer ๊ฐ์ฒด๋กœ ์—ฌ๋Ÿฌ ์Šฌ๋ผ์ด์Šค(reducer)๋ฅผ ํ•˜๋‚˜๋กœ ํ†ตํ•ฉํ•ฉ๋‹ˆ๋‹ค.
// store/reducer/reducer.ts
import { boardsReducer } from '../slices/boardsSlice';
import { loggerReducer } from '../slices/loggerSlice';
import { modalReducer } from '../slices/modalSlice';

const reducer = {
  logger: loggerReducer,
  boards: boardsReducer,
  modal: modalReducer,
};

export default reducer;

์Šคํ† ์–ด ์„ค์ •

  • configureStore๋Š” ์—ฌ๋Ÿฌ slice๋ฅผ ํ•˜๋‚˜๋กœ ํ•ฉ์น˜๊ณ  Redux store๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import reducer from './reducer/reducer';

const store = configureStore({
  reducer,
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;



4. ํƒ€์ž… ์ปค์Šคํ…€ ํ›… ์„ค์ • ( hooks.ts )

React ์ปดํฌ๋„ŒํŠธ์—์„œ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ dispatch์™€ selector๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ค๋‹ˆ๋‹ค.

  • useDispatch๋Š” ์ปดํฌ๋„ŒํŠธ์—์„œ ์•ก์…˜์„ ์‹คํ–‰์‹œํ‚ค๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

  • useSelector๋Š” Redux store์— ์ €์žฅ๋œ ์ƒํƒœ๊ฐ’์„ ๊บผ๋‚ด์˜ต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ทธ ๊ฐ’์ด ๋ฐ”๋€Œ๋ฉด, ์ž๋™์œผ๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฆฌ๋ Œ๋”๋งํ•ด์ค๋‹ˆ๋‹ค.

import { AppDispatch, RootState } from '../store';
import { TypedUseSelectorHook, useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';

export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useTypedDispatch = () => useDispatch<AppDispatch>();

๐Ÿค” useSelector๊ฐ€ ํ•„์š”ํ•œ ์ด์œ ๋Š” ๋ญ˜๊นŒ?

React๋Š” Redux store์˜ ๊ฐ’์„ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์–ด๋–ค ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ด€์ฐฐํ• ์ง€ ์ง์ ‘ ์•Œ๋ ค์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ฐœ๋…React (useState)Redux (useSelector, useDispatch)
์ƒํƒœ ์ €์žฅ์ปดํฌ๋„ŒํŠธ ์•ˆ (useState)์ค‘์•™ ์ €์žฅ์†Œ์ธ Redux store
์ƒํƒœ ๋ณ€๊ฒฝsetState(newVal)dispatch(action)
๋ฆฌ๋ Œ๋”๋ง์ž๋™useSelector๋กœ ๊ด€์ฐฐํ•˜๊ณ  ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ๋งŒ ์ž๋™



5. ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ ( BoardList.tsx )

๋ฒ„ํŠผ ํด๋ฆญ์œผ๋กœ ๋ณด๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , Redux ์ƒํƒœ๋ฅผ ํ™”๋ฉด์— ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.

// BoardList.tsx
import React, { useRef, useState } from 'react';
import { useTypedSelector } from '../../hooks/redux';
import SideForm from './SideForm/SideForm';
import { FiPlusCircle } from 'react-icons/fi';
import {
  addButton,
  addSection,
  boardItem,
  boardItemActive,
  container,
  title,
} from './BoardList.css';
import clsx from 'clsx';

type TBoardListProps = {
  activeBoardId: string;
  setActiveBoardId: React.Dispatch<React.SetStateAction<string>>;
};

const BoardList: React.FC<TBoardListProps> = ({
  activeBoardId,
  setActiveBoardId,
}) => {
  const { boardArray } = useTypedSelector((state) => state.boards);
  const [isFormOpen, setIsFormOpen] = useState(false);
  const inputRef = useRef<HTMLInputElement>(null);

  const handleClick = () => {
    setIsFormOpen(!isFormOpen);
    setTimeout(() => {
      inputRef.current?.focus();
    }, 0);
  };

  return (
    <div className={container}>
      <div className={title}>๊ฒŒ์‹œํŒ:</div>
      {boardArray.map((board, index) => (
        <div
          key={board.boardId}
          onClick={() => setActiveBoardId(boardArray[index].boardId)}
          className={clsx(
            {
              [boardItemActive]:
                boardArray.findIndex((b) => b.boardId === activeBoardId) ===
                index,
            },
            {
              [boardItem]:
                boardArray.findIndex((b) => b.boardId === activeBoardId) !==
                index,
            }
          )}
        >
          <div>{board.boardName}</div>
        </div>
      ))}
      <div className={addSection}>
        {isFormOpen ? (
          <SideForm inputRef={inputRef} setIsFormOpen={setIsFormOpen} />
        ) : (
          <FiPlusCircle className={addButton} onClick={handleClick} />
        )}
      </div>
    </div>
  );
};

export default BoardList;
  • useRef() : ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ์—์„œ ref๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์“ฐ๋Š” ํ›…์ž…๋‹ˆ๋‹ค.

ref๋ž€?

React ์š”์†Œ๋‚˜ ๊ฐ’์— ๋Œ€ํ•œ โ€œ์ง์ ‘์ ์ธ ์ฐธ์กฐโ€ ๋ฅผ ์˜๋ฏธํ•˜๋ฉฐ DOM ์š”์†Œ๋‚˜ ์ปดํฌ๋„ŒํŠธ์— ์ง์ ‘ ์ ‘๊ทผํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

const myRef = useRef(null);

myRef.current๊ฐ€ ์‹ค์ œ ๊ฐ€๋ฆฌํ‚ค๋Š” ๋Œ€์ƒ์ด ๋ฉ๋‹ˆ๋‹ค.

[์‚ฌ์šฉ ์˜ˆ์‹œ]

  • input ํฌ์ปค์Šค ์ฃผ๊ธฐ
  • ์Šคํฌ๋กค ์œ„์น˜ ํ™•์ธ ๋˜๋Š” ์กฐ์ž‘
  • ๋น„๋””์˜ค ์ปจํŠธ๋กค๋Ÿฌ ๋‹ค๋ฃจ๊ธฐ

โš ๏ธ ๋ณดํ†ต์˜ ์ƒํ™ฉ์—์„œ ๋ฆฌ์•กํŠธ๋Š” Virtual DOM ๊ธฐ๋ฐ˜์œผ๋กœ ์ž‘๋™ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ง์ ‘ DOM์„ ๊ฑด๋“œ๋ฆฌ๋ฉด ๋ฆฌ์•กํŠธ์—์„œ๋Š” ๋ฐ”๋€ ๊ฒƒ์„ ๋ชจ๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๋™์ž‘ ํ๋ฆ„

์ถœ์ฒ˜: ๋ฆฌ๋•์Šค ๊ณต์‹ ๋ฌธ์„œ

[React ์ปดํฌ๋„ŒํŠธ]
   |
   | ํด๋ฆญ (์˜ˆ: ๋ณด๋“œ ์ถ”๊ฐ€ ๋ฒ„ํŠผ)
   โ†“
dispatch(addBoard) ํ˜ธ์ถœ
   |
   โ†“
[Reducer ์‹คํ–‰]
createSlice ๋‚ด๋ถ€์˜ addBoard ํ•จ์ˆ˜ ์‹คํ–‰
   |
   โ†“
[Redux store ์ƒํƒœ ๋ณ€๊ฒฝ]
์ƒˆ ๋ณด๋“œ๊ฐ€ board ๋ฐฐ์—ด์— ์ถ”๊ฐ€๋จ
   |
   โ†“
[useSelector ๊ตฌ๋… ์ค‘]
board ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์œผ๋ฏ€๋กœ ์ปดํฌ๋„ŒํŠธ ๋ฆฌ๋ Œ๋”๋ง



๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ณ„ ์—ญํ•  ์ •๋ฆฌ


๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์‚ฌ์šฉ๋œ ํ•จ์ˆ˜์—ญํ•  ์„ค๋ช…
@reduxjs/toolkitcreateSlice์ƒํƒœ์™€ reducer๋ฅผ ํ•œ ๋ฒˆ์— ์ƒ์„ฑํ•จ
configureStore์—ฌ๋Ÿฌ reducer๋ฅผ ํ•ฉ์ณ store ์ƒ์„ฑ
PayloadAction์•ก์…˜์˜ payload ํƒ€์ž… ๋ช…์‹œ
react-reduxuseDispatch์ปดํฌ๋„ŒํŠธ์—์„œ ์•ก์…˜์„ ๋””์ŠคํŒจ์น˜ (๋ณด๋‚ด๊ธฐ)
useSelector์ปดํฌ๋„ŒํŠธ์—์„œ ์ƒํƒœ๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ๊ฐ€์ ธ์˜ค๊ธฐ
TypedUseSelectorHookuseSelector์— ํƒ€์ž…์„ ๋ถ™์—ฌ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ธฐ



โœ๏ธ ํšŒ๊ณ 

๊ทธ๋ƒฅ ๋ง‰์ƒ ๋”ฐ๋ผ์น˜๊ธฐ๋ณด๋‹ค ์ •๋ฆฌํ•˜๋ฉด์„œ ๋” ์ดํ•ดํ•˜๋Š” ๊ฒŒ ๋” ๋„์›€์ด ๋˜์—ˆ๋‹ค. ์•„์ง์€ ์–ด์ƒ‰ํ•˜์ง€๋งŒ ๋” ๋ฐ˜๋ณต ํ•™์Šต์ด ํ•„์š”ํ•  ๊ฒƒ ๊ฐ™๋‹ค.

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

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