Redux
자바스크립트 앱에서 상태(state)를 예측 가능하게 관리할 수 있도록 도와주는 상태 관리 라이브러리
애플리케이션의 상태를 하나의 스토어(store)에서 관리하고, 상태 변경은 액션(action)이라는 이벤트를 통해서만 이루어짐
사용 이유
- 여러 컴포넌트가 같은 데이터를 써야 할 때, 상태 공유가 복잡해짐
- Props drilling(깊은 자식에게 props 전달)이 심해짐
- 상태 흐름이 예측되지 않음 ➡️ 디버깅 어려움
핵심 원칙
- 단일 스토어(Single Source of Truth): 애플리케이션의 모든 상태는 하나의 스토어에 저장됨
- 상태는 읽기 전용(State is Read-Only): 상태를 직접 변경할 수 없고, 액션을 통해서만 변경할 수 있음
- 변경은 순수 함수로만(Changes are made with Pure Functions): 리듀서(Reducer)라는 순수 함수가 이전 상태와 액션을 받아 새로운 상태를 반환
핵심 구성 요소
Store | 상태가 저장되는 전역 저장소 |
State | Store에 저장된 현재 상태 값 |
Action | 상태에 어떤 변화가 일어날지를 설명하는 객체 |
Reducer | 액션에 따라 상태를 변화시키는 순수 함수 |
Dispatch | 액션을 스토어에 보내는 함수 |
Subscribe | 상태 변경을 감지하여 리스너를 실행 |
데이터 흐름
UI (사용자 인터렉션)
↓
dispatch(action)
↓
reducer(state, action)
↓
새로운 state 반환
↓
store 업데이트 → UI 리렌더링
Redux 사용법
설치
npm install @reduxjs/toolkit react-redux
🔧 @reduxjs/toolkit 설치 이유
- Redux 공식에서 권장하는 도구 모음
- Redux 사용 시 자주 겪는 보일러플레이트 코드(반복되는 코드)를 줄여줌
- createSlice, configureStore 등 편리한 함수 제공
- 불변성 유지도 자동 처리 (Immer 내장)
store 구성
- configureStore을 통해 스토어 구성 간편화
import { configureStore } from '@reduxjs/toolkit'
import { counterSlice } from './counterSlice'
export default configureStore({
reducer: {
counter: counterSlice.reducer,
},
})
createSlice
- Redux state 조각(Slice)을 정의하는 함수
- 액션 + 리듀서를 한 번에 작성 가능
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
count: 1,
label: '카운트',
},
reducers: {
increament: (state, action) => {
state.count += action.payload || 1 // 불변성? → OK! Immer가 내부적으로 처리
},
decreament: state => {
state.count -= 1
},
reset: state => {
state.count = 0
},
},
})
export const { increament, decreament, reset } = counterSlice.actions
export default counterSlice.reducer
👉🏻 reducers
- 상태가 어떻게 바뀔지 정의하는 함수들을 담고 있음
- 각각이 자동으로 액션 생성자도 만들어줌
👉🏻 state
- initialState의 현재 상태
- 리듀서 함수의 첫 번째 매개변수로 전달됨
- 상태를 어떻게 바꿀지 여기서 정의
initialState: {
count: 1,
label: '카운트',
}
decreament: (state) => {
state.count -= 1
}
👉🏻 action
- 리듀서 함수의 두 번째 매개변수로 전달됨
- 주로 payload에 전달한 값이 담겨있음
increament: (state, action) => {
state.count += action.payload || 1
}
dispatch(increament(3))
React와 연결
- Provider로 store 주입
// main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import router from './router.jsx'
import './index.css'
// Redux
import { Provider } from 'react-redux'
import store from './store/store.js'
createRoot(document.getElementById('root')).render(
<StrictMode>
<Provider store={store}>
<RouterProvider router={router} fallbackElement={<div>로딩중...</div>} />
</Provider>
</StrictMode>
)
컴포넌트에서 사용
- useSelector로 store에 있는 상태를 읽어옴
- state.[slice 이름] 또는 state.[slice 이름].[속성] 형태로 접근
- slice 이름 = store에서 등록한 리듀서 키
import React from 'react'
import { useSelector } from 'react-redux'
const Counter = () => {
const { count, label } = useSelector(state => state.counter)
return (
<p>
{label}: {count}
</p>
)
}
export default Counter
- store에 있는 함수를 실행할 때는 dispatch로 감싸서 보내야 함
import Counter from '@/components/Counter'
import { decreament, increament, reset } from '@/store/counterSlice'
import React from 'react'
import { useDispatch } from 'react-redux'
const BlogPage = () => {
const dispatch = useDispatch()
return (
<main>
<h2>BlogPage</h2>
<Counter />
<button onClick={() => dispatch(increament())}>증가하기</button>
<button onClick={() => dispatch(increament(10))}>증가하기(10)</button>
<button onClick={() => dispatch(decreament())}>감소하기</button>
<button onClick={() => dispatch(reset())}>리셋하기</button>
</main>
)
}
export default BlogPage
Redux 비동기 처리
createAsyncThunk
Redux Toolkit에서 제공하는 함수로, 비동기 작업을 처리하는 액션 생성자를 만듦
- 용도: Promise 기반 비동기 로직을 포함하는 Redux 액션을 쉽게 만들기 위한 함수
- 동작: 내부적으로 3가지 액션 타입을 자동으로 생성
pending | 요청 중 |
fulfilled | 요청 성공 |
rejected | 요청 실패 |
// todoSlice.js
export const fetchTodos = createAsyncThunk('todo/fetchTodos', async () => {
const response = await getTodosData()
return response
})
✅ 첫 번째 매개변수: 'todo/fetchTodos'
- 액션 타입의 접두사를 정의
- 일반적으로 '슬라이스 이름/액션 이름' 형태로 작성
- 내부적으로 세 가지 액션 타입이 생성됨
- todos/fetchTodos/pending: 요청 시작 시
- todos/fetchTodos/fulfilled: 요청 성공 시
- todos/fetchTodoss/rejected: 요청 실패 시
✅ 두 번째 매개변수: async(queryParams = '', { rejectWithValue }) => { ... }
- 비동기 작업을 수행하는 함수
- 함수 내부의 결과에 따라 pending, fulfilled, rejected
- 첫 번째 인자 queryParams
- 액션 디스패치 시 전달할 수 있는 매개변수
- 기본값 ''이 설정되어 있어 인자없이 호출 가능
- 두번째 인자 { rejectWithValue }
- thunkAPI 객체의 구조 분해 할당
- thunkAPI 속성
dispatch | 스토어의 dispatch 함수 |
getState | 현재 스토어 상태를 가져오는 함수 |
extra | 미들웨어에 전달된 extra 인자 |
requestId | 현재 요청의 고유 ID |
signal | 요청 취소에 사용할 수 있는 AbortController.signal 객체 |
rejectWithValue | 에러 처리를 위한 유틸리티 함수 |
fulfillWithValue | 성공 처리를 위한 유틸리티 함수 |
extraReducers
외부에서 정의된 액션(createAsyncThunk로 생성된 액션: fetchTodos)에 응답하는 리듀서를 정의
- builder.addCase: 특정 액션 타입에 대한 리듀서를 추가
// todoSlice.js
export const todoSlice = createSlice({
name: 'todo',
initialState: {
todos: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed',
error: null,
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, state => {
state.status = 'loading'
state.error = null
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = 'succeeded'
state.todos = action.payload
state.error = null
})
.addCase(fetchTodos.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
},
})
🔄 fetchTodos.pending | 데이터 요청이 시작되었을 때 실행 |
✅ fetchTodos.fulfilled | 데이터 요청이 성공했을 때 실행 |
❌ fetchTodos.rejected | 데이터 요청이 실패했을 때 실행 |
컴포넌트에서 dispatch로 호출
// TodoList.jsx
import { fetchTodos } from '@/store/todoSlice'
import React, { useEffect } from 'react'
import ListGroup from 'react-bootstrap/ListGroup'
import { useDispatch, useSelector } from 'react-redux'
const TodoList = () => {
const dispatch = useDispatch()
const { todos, status, error } = useSelector(state => state.todo)
useEffect(() => {
dispatch(fetchTodos()) // dispatch로 fetchTodos(비동기 액션 생성 함수)을 호출해야 getTodosData()가 호출되어 실행됨
}, [dispatch])
if (status === 'loading') return <div>Loading...</div>
if (status === 'failed') return <div>{error}</div>
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.desc}</p>
<p className="m-2" style={{ fontSize: '0.75em' }}>
{todo.createdAt}
</p>
<i className="bi bi-trash"></i>
</ListGroup.Item>
))}
</ListGroup>
)
}
export default TodoList
728x90
반응형
'LG 유플러스 유레카 SW > React' 카테고리의 다른 글
[#63] 날씨 오픈 API 연동 (1) | 2025.04.28 |
---|---|
[#62] 용돈 기입장 프로젝트 (feat. React) (0) | 2025.04.28 |
[#60] React - Hook + 성능 최적화 정리 (0) | 2025.04.23 |
[#59] React 쇼핑몰 - Shop 페이지 상품 필터링/정렬 + 페이지네이션 (0) | 2025.04.22 |
[#58] React 쇼핑몰 - 장바구니 수정/삭제 + 반응형 Shop 페이지 (0) | 2025.04.21 |