[Day61] 쇼핑몰 실습 - Redux Toolkit
[Day61] 쇼핑몰 실습 - Redux Toolkit
React Redux Quick Start (Redux Toolkit)
🔸 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 함수 실행됨.
✅ 전체 흐름 요약
- slice 파일을 만들고 reducer 함수 + actions 생성
- store에 slice 등록
<Provider>
로 React 앱 감싸기- 컴포넌트에서
useSelector
,useDispatch
로 state 읽기 / 변경
dark mode (with Redux)
🔸 Redux 적용 전 로컬 상태 기반
구현 흐름
- 컴포넌트에서
useState
로 다크모드 상태를 관리함 useEffect
에서localStorage
에 저장된theme
값을 읽어서 상태 초기화- 컴포넌트에서
Redux
상태 조회 (useSelector
) - 테마 변경 버튼 클릭 →
toggleTheme()
액션 디스패치 - Redux 스토어의 상태가 변경됨
- 상태 변경을 감지하는
useEffect
→localStorage
와document.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 적용
개발 흐름
- 전역 상태에서
isDarkMode
를 Redux로 관리 localStorage
에서 초기 테마 상태를 가져옴 →themeSlice
의initialState
로 설정toggleTheme
액션으로 전역 상태 변경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 모드 적용
dark 모드 해제
TODOS - Redux Toolkit으로 비동기 상태 관리
✅ 전체 흐름 요약
- API 함수 작성 (axios)
- createAsyncThunk + createSlice로 비동기 로직 & 상태 정의
- store에 slice 등록
- 컴포넌트에서
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.