์กฐ์•„๋งˆ์‹œ

์“ธ๋ชจ ์žˆ๋Š” ์ƒ์„ธํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ

์›น๊ฐœ๋ฐœ/reactjs

Redux + TypeScript ํ™•์žฅ ๊ฐœ๋… ์™„์ „ ์ดํ•ดํ•˜๊ธฐ

joamashi 2025. 4. 14. 19:52
๋ฐ˜์‘ํ˜•

๐Ÿ‘ฃ ๋จผ์ € ์ค€๋น„ํ•˜๊ธฐ (๊ณตํ†ต ์…‹์—…)

1. ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜

npm install @reduxjs/toolkit react-redux redux-persist

2. Redux ์Šคํ† ์–ด ๋งŒ๋“ค๊ธฐ (src/app/store.ts)

import { configureStore } from '@reduxjs/toolkit'
import todoReducer from '../features/todo/todoSlice'
import authReducer from '../features/auth/authSlice'
import themeReducer from '../features/theme/themeSlice'
import filterReducer from '../features/filter/filterSlice'

// Redux ์Šคํ† ์–ด ์ƒ์„ฑ - ๋ชจ๋“  slice๋ฅผ ํ•˜๋‚˜๋กœ ํ•ฉ์นฉ๋‹ˆ๋‹ค
export const store = configureStore({
  reducer: {
    todo: todoReducer,
    auth: authReducer,
    theme: themeReducer,
    filter: filterReducer,
  },
})

// ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์šฉ ํƒ€์ž… ์ •์˜
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

3. ์ปค์Šคํ…€ ํ›… ์ถ”๊ฐ€ (src/app/hooks.ts)

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

// ๋””์ŠคํŒจ์น˜์™€ ์…€๋ ‰ํ„ฐ๋ฅผ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์ปค์Šคํ…€ ํ›…
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

โœ… 1. Todo ๊ธฐ๋Šฅ

๐Ÿ“ src/features/todo/todoSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

// ํ•  ์ผ ํ•˜๋‚˜์˜ ํƒ€์ž… ์ •์˜
export interface Todo {
  id: string
  title: string
  completed: boolean
  tags?: string[]
}

// ์ƒํƒœ๋Š” Todo ๋ฐฐ์—ด
const initialState: Todo[] = []

export const todoSlice = createSlice({
  name: 'todo',
  initialState,
  reducers: {
    addTodo: (state, action: PayloadAction<Todo>) => {
      state.push(action.payload) // ์ƒˆ๋กœ์šด ํ•  ์ผ์„ ์ถ”๊ฐ€
    },
    toggleComplete: (state, action: PayloadAction<string>) => {
      const todo = state.find(t => t.id === action.payload)
      if (todo) {
        todo.completed = !todo.completed
      }
    },
    deleteTodo: (state, action: PayloadAction<string>) => {
      return state.filter(t => t.id !== action.payload)
    },
  },
})

export const { addTodo, toggleComplete, deleteTodo } = todoSlice.actions
export default todoSlice.reducer

๐Ÿ” 2. ์œ ์ € ์ธ์ฆ

๐Ÿ“ src/features/auth/authSlice.ts

import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'

// ๋กœ๊ทธ์ธ ๋น„๋™๊ธฐ thunk
export const login = createAsyncThunk(
  'auth/login',
  async ({ id, pw }: { id: string; pw: string }) => {
    // ์˜ˆ์ œ์šฉ: ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ์œ ์ € ์ •๋ณด์™€ ํ† ํฐ ๋ฐ˜ํ™˜
    return {
      user: id,
      token: 'token_example',
    }
  }
)

interface AuthState {
  user: string | null
  token: string | null
  loading: boolean
}

const initialState: AuthState = {
  user: null,
  token: null,
  loading: false,
}

const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {
    logout: (state) => {
      state.user = null
      state.token = null
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(login.pending, (state) => {
        state.loading = true
      })
      .addCase(login.fulfilled, (state, action) => {
        state.user = action.payload.user
        state.token = action.payload.token
        state.loading = false
      })
      .addCase(login.rejected, (state) => {
        state.loading = false
      })
  },
})

export const { logout } = authSlice.actions
export default authSlice.reducer

๐ŸŒ™ 3. ๋‹คํฌ ๋ชจ๋“œ

๐Ÿ“ src/features/theme/themeSlice.ts

import { createSlice } from '@reduxjs/toolkit'

interface ThemeState {
  mode: 'light' | 'dark'
}

const initialState: ThemeState = {
  mode: 'light',
}

const themeSlice = createSlice({
  name: 'theme',
  initialState,
  reducers: {
    toggleTheme: (state) => {
      state.mode = state.mode === 'light' ? 'dark' : 'light'
    },
  },
})

export const { toggleTheme } = themeSlice.actions
export default themeSlice.reducer

๐Ÿ’ก ์ ์šฉ ๋ฐฉ๋ฒ• (Tailwind ๋˜๋Š” ํด๋ž˜์Šค ๋ณ€๊ฒฝ)

const mode = useAppSelector(state => state.theme.mode)
useEffect(() => {
  document.body.className = mode
}, [mode])

๐Ÿท๏ธ 4. ํƒœ๊ทธ ํ•„ํ„ฐ๋ง

๐Ÿ“ src/features/filter/filterSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

interface FilterState {
  tags: string[]
}

const initialState: FilterState = {
  tags: [],
}

const filterSlice = createSlice({
  name: 'filter',
  initialState,
  reducers: {
    setTags: (state, action: PayloadAction<string[]>) => {
      state.tags = action.payload
    },
  },
})

export const { setTags } = filterSlice.actions
export default filterSlice.reducer

๐Ÿ’ก ํƒœ๊ทธ ๊ธฐ์ค€์œผ๋กœ Todo ํ•„ํ„ฐ๋ง

const todos = useAppSelector((state) => state.todo)
const filterTags = useAppSelector((state) => state.filter.tags)

const filteredTodos = todos.filter(todo =>
  filterTags.length === 0 || todo.tags?.some(tag => filterTags.includes(tag))
)

๐Ÿ“ฆ ์ „์ฒด ํด๋” ๊ตฌ์กฐ ์š”์•ฝ

src/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ store.ts
โ”‚   โ””โ”€โ”€ hooks.ts
โ”œโ”€โ”€ features/
โ”‚   โ”œโ”€โ”€ auth/
โ”‚   โ”‚   โ””โ”€โ”€ authSlice.ts
โ”‚   โ”œโ”€โ”€ todo/
โ”‚   โ”‚   โ””โ”€โ”€ todoSlice.ts
โ”‚   โ”œโ”€โ”€ theme/
โ”‚   โ”‚   โ””โ”€โ”€ themeSlice.ts
โ”‚   โ””โ”€โ”€ filter/
โ”‚       โ””โ”€โ”€ filterSlice.ts
โ””โ”€โ”€ components/
    โ”œโ”€โ”€ TodoList.tsx
    โ”œโ”€โ”€ TodoItem.tsx
    โ””โ”€โ”€ LoginForm.tsx

โœจ ๋‹ค์Œ์— ์ด์–ด์„œ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋“ค

  • redux-persist๋กœ ์ƒํƒœ ์œ ์ง€
  • ๊ฐ ๊ธฐ๋Šฅ์„ ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ค๊ธฐ (<TodoList />, <LoginForm />, <TagFilter />)
  • Zustand๋กœ ๋Œ€์ฒดํ•˜๋Š” ๋ฐฉ์‹
  • Firebase ์ธ์ฆ ์—ฐ๋™
728x90
๋ฐ˜์‘ํ˜•