Post

[Day61] 쇼핑몰 실습 - Redux Toolkit

[Day61] 쇼핑몰 실습 - Redux Toolkit

React Redux Quick Start (Redux Toolkit)

React-Redux

🔸 Redux란 ?

  • Redux는 애플리케이션의 상태(state)를 예측 가능하게 관리하는 라이브러리.
  • React와 함께 쓰면 전역 상태 관리를 더 효율적으로 할 수 있음.

1. Install Redux Toolkit and React Redux설치

1
npm install @reduxjs/toolkit react-redux
  • @reduxjs/toolkit: Redux의 공식 툴킷. 보일러플레이트 줄여주고 구조 잡기 쉬움.
    • 보일러플레이트 : 반복적으로 작성해야 하는 코드
  • react-redux: Redux를 React에 연결해주는 라이브러리.

2. Create a Redux State Slice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createSlice } from '@reduxjs/toolkit'
// slice로 만든걸 store에 연결 해줘야함
export const counterSlice = createSlice({
  name: 'counter', // slice의 이름은 고유해야함. counter의 state를 관리할 것임
  initialState: {
    count: 1,
    label: '카운터',
  }, // 관리할 내용
  reducers: {
    increment: (state, action) => {
      state.count += action.payload
    },
    decrement: state => {
      state.count -= 1
    },
    reset: state => {
      state.count = 0
    },
  }, // state가 변경되는 변경함수가 등록되는 곳, 얘를 쓰려면 함수 이름을 각각 export해야됨
})

export const { increment, decrement, reset } = counterSlice.actions // ? 왜 actions
  • createSlice는 액션 생성자 + 리듀서 함수를 자동으로 만들어 줌.
  • Immer덕분에 state.value += 1처럼 불변성을 깨지 않고 직접 변경 가능

3. Add Slice Reducers to the Store (스토어 설정)

1
2
3
4
5
6
7
8
import { configureStore } from '@reduxjs/toolkit'
import { counterSlice } from './counterSlice'

export default configureStore({
  reducer: {
    counter: counterSlice.reducer,
  },
})
  • configureStore: Redux Toolkit에서 제공하는 함수
    • createStore보다 설정이 간편하고 기본 devTools, thunk 설정 포함됨.
  • 여러 slice를 reducer 객체로 묶어서 사용.

4. Provide the Redux Store to React

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import router from './router.jsx'
import store from './store/store'
import { Provider } from 'react-redux'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <Provider store={store}>
      <RouterProvider router={router} fallbackElement={<div>로딩중...</div>} />
    </Provider>
  </StrictMode>
)
  • <Provider>는 Redux 스토어를 React 앱 전체에 주입해주는 컴포넌트.

5. Use Redux State and Actions in React Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from 'react'
import { decrement, increment, reset } from '@/store/counterSlice'
import { useSelector, useDispatch } from 'react-redux'

const BlogPage = () => {
  const dispatch = useDispatch()
  const countData = useSelector(state => state.counter)
  const { count, label } = countData

  const increase = (num = 1) => dispatch(increment(num))
  const decrease = () => dispatch(decrement())
  const restart = () => dispatch(reset())

  return (
    <main>
      <p>{label} : {count}</p>
      <button onClick={() => increase()}>증가하기</button>
      <button onClick={() => decrease()}>감소하기</button>
      <button onClick={() => restart()}>초기화</button>
    </main>
  )
}

export default BlogPage
  • useSelector: store의 state를 읽어옴 (구독).
  • useDispatch: action을 실행시켜 state를 변경할 수 있게 해줌.
  • dispatch(increment({ payload: 1 })): reducer 안의 increment 함수 실행됨.

✅ 전체 흐름 요약

  1. slice 파일을 만들고 reducer 함수 + actions 생성
  2. store에 slice 등록
  3. <Provider>로 React 앱 감싸기
  4. 컴포넌트에서 useSelector, useDispatch로 state 읽기 / 변경


dark mode (with Redux)

🔸 Redux 적용 전 로컬 상태 기반

구현 흐름

  1. 컴포넌트에서 useState로 다크모드 상태를 관리함
  2. useEffect에서 localStorage에 저장된 theme 값을 읽어서 상태 초기화
  3. 컴포넌트에서 Redux 상태 조회 (useSelector)
  4. 테마 변경 버튼 클릭 → toggleTheme() 액션 디스패치
  5. Redux 스토어의 상태가 변경됨
  6. 상태 변경을 감지하는 useEffectlocalStoragedocument.body에 반영

document.body.classList.toggle('dark-mode', parsedTheme)는 무슨 의미일까 ?

📌 classList.toggle() 기본 사용법

1
element.classList.toggle('클래스명')
  • 클래스가 없으면 추가, 있으면 제거


📌 조건을 추가한 사용법

1
element.classList.toggle('클래스명', 조건)
  • 조건true추가
  • 조건false제거

코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const [isDarkMode, setIsDarkMode] = useState(false)

useEffect(() => {
    const savedTheme = localStorage.getItem('theme')
    if (savedTheme !== null) {
      const parsedTheme = JSON.parse(savedTheme)
      setIsDarkMode(parsedTheme)
      document.body.classList.toggle('dark-mode', parsedTheme)
    }
}, [])

const handleThemeToggle = () => {
    const newTheme = !isDarkMode
    setIsDarkMode(newTheme)
    localStorage.setItem('theme', JSON.stringify(newTheme))
}
  • 읽을 땐 parse, 저장할 땐 stringify

🔸 Redux 적용

개발 흐름

  1. 전역 상태에서 isDarkMode를 Redux로 관리
  2. localStorage에서 초기 테마 상태를 가져옴 → themeSliceinitialState로 설정
  3. toggleTheme 액션으로 전역 상태 변경
  4. useEffect로 상태가 변경될 때마다:
    • localStorage에 저장
    • document.body.classList.add/remove로 DOM 업데이트

1. 상태(State) 정의 - themeSlice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createSlice } from '@reduxjs/toolkit'

const savedTheme = localStorage.getItem('theme')
const isDarkTheme = savedTheme !== null ? JSON.parse(savedTheme) : false
export const themeSlice = createSlice({
  name: 'theme',
  initialState: { 
    isDarkMode: isDarkTheme,
  },
  reducers: {
    toggleTheme: state => {
      state.isDarkMode = !state.isDarkMode
    },
  },
})

export const { toggleTheme } = themeSlice.actions
  • 전역으로 관리할 isDarkMode 상태를 정의
  • 초기값은 localStorage에서 불러와서 설정
  • createSlice를 통해 상태와 액션을 동시에 정의

2. Redux Store 설정

1
2
3
4
5
6
7
8
9
10
import { configureStore } from '@reduxjs/toolkit'
import { counterSlice } from './counterSlice'
import { themeSlice } from './themeSlice'

export default configureStore({
  reducer: {
    counter: counterSlice.reducer,
    theme: themeSlice.reducer,
  },
})
  • themeSlice.reduce를 스토어에 등록해서 글로벌 상태로 연결

3. 컴포넌트에서 Redux 상태 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const { isDarkMode } = useSelector(state => state.theme) // 상태 조회
const dispatch = useDispatch() 

const handleThemeToggle = () => {
    dispatch(toggleTheme()) // 액션 디스패치
}

useEffect(() => {
    localStorage.setItem('theme', JSON.stringify(isDarkMode))
    if (isDarkMode) {
      document.body.classList.add('dark-mode')
    } else {
      document.body.classList.remove('dark-mode')
    }
}, [isDarkMode])
  • Redux 상태가 바뀌면 테마 설정을 실제 DOM과 localStorage에 반영

적용된 UI

dark 모드 적용

bodydark-mode 추가된 모습

dark 모드 해제

bodydark-mode 제거된 모습


TODOS - Redux Toolkit으로 비동기 상태 관리

✅ 전체 흐름 요약

  1. API 함수 작성 (axios)
  2. createAsyncThunk + createSlice로 비동기 로직 & 상태 정의
  3. store에 slice 등록
  4. 컴포넌트에서 dispatch, useSelector로 사용

🔸 서버에서 todos 가져오기 - todosApi.js

1
2
3
4
5
6
7
8
9
10
import axios from 'axios'

export const getTodosData = async () => {
  try {
    const res = await axios.get(`/api/todos`)
    return res.data
  } catch (error) {
    console.log(error)
  }
}
  • 외부 API (또는 mock 서버 등)에서 데이터를 가져오는 비동기 함수 정의
  • 나중에 createAsyncThunk에서 호출됨

🔸 비동기 actions + 상태 slice 정의 - todosSlice.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { getTodosData } from '@/api/todosApi'
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'

// 1. 비동기 thunk action 생성
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
  const res = await getTodosData()
  return res
})
// 2. slice 정의
export const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    todos: [],
    status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
    error: null,
  },
  extraReducers(builder) {
    builder
      .addCase(fetchTodos.pending, status => {
        status.status = 'loading'
        status.error = null
      })
      .addCase(fetchTodos.fulfilled, (status, action) => {
        status.status = 'succeeded'
        status.todos = action.payload
        status.error = null
      })
      .addCase(fetchTodos.rejected, (status, action) => {
        status.status = 'failed'
        status.error = action.error.message
      })
  },
})
  • createAsyncThunk를 통해 비동기 요청과 상태 관리 쉽게 처리
  • extraReducers에서 요청 상태에 따라 처리 분기

🔸 store에 slice 등록

1
2
3
4
5
6
7
8
9
10
import { configureStore } from '@reduxjs/toolkit'
import { counterSlice } from './countSlice'
import { todosSlice } from './todosSlice'

export default configureStore({
  reducer: {
    counter: counterSlice.reducer,
    todos: todosSlice.reducer,
  },
})

🔸 컴포넌트 사용 - TodoList.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchTodos } from '@/store/todoSlice'
import ListGroup from 'react-bootstrap/ListGroup'

const TodoList = () => {
  const dispatch = useDispatch()
  const { todos, status, error } = useSelector(state => state.todos)

  useEffect(() => {
    dispatch(fetchTodos())
  }, [dispatch])

  if (status === 'loading') return <div>Loading...</div>
  if (status === 'failed') return <div>{error}</div>

  if (status)
    return todos.length === 0 ? (
      <div></div>
    ) : (
      <ListGroup>
        {todos.map(todo => (
          <ListGroup.Item
            key={todo.id}
            className="d-flex justify-content-between align-items-center"
          >
            <p className=" flex-grow-1">{todo.todosDesc}</p>
            <p className=" m-2" style=>
              {todo.createAt}
            </p>
            <i className="bi bi-trash"></i>
          </ListGroup.Item>
        ))}
      </ListGroup>
    )
}

export default TodoList
  • useDispatch로 thunk action 호출 (fetchTodos)
  • useSelector로 전역 상태 접근


END

This post is licensed under CC BY 4.0 by the author.