Action 이벤트를 사용하여 애플리케이션 상태를 관리하고 업데이트하기 위한 패턴 및 라이브러리
모든 UI 프레임워크와 통합할 수 있고, React와 함께 가장 자주 사용된다.
독립형 JS 라이브러리이므로 UI 프레임워크가 없어도 사용 가능
전역 상태가 예측 가능한 방식으로만 업데이트될 수 있도록 상태에 대한 중앙 집중식 저장소 역할을 한다.
아래의 그림과 같이 애플리케이션은
State,
현재 상태에 대한 UI인 View,
사용자의 입력에 따라 앱에서 발생하는 이벤트와 State에서의 업데이트인 Trigger로 이루어져 있는데,
Redux는 공유된 상태가 있는 경우 이를 단방향 데이터 흐름으로 관리되게 함으로써,
상태 관리와 관련된 개념을 정의 및 분리하고 뷰와 상태 간의 독립성을 유지하는 규칙을 적용함으로써 코드에 더 많은 구조와 유지 관리성을 보장 받는다.
공유 상태 관리를 처리하는데 도움이 되지만 배워야 할 개념과 작성해야 할 코드가 많다.
따라서 다음의 경우에 유용하다.
type 필드가 있는 JS 객체로 애플리케이션에서 발생한 이벤트
Ex.
const addTodoAction = {
type: 'todos/todoAdded', // (기능)/(발생 이벤트)
payload: 'Buy milk' // 추가 정보로 필수 아님
}
현재 state와 Action 객체를 수신하고 필요한 경우 상태를 업데이트하는 방법을 결정하고 새 상태를 반환하는 함수
(state, action) => newState
state 및 action 인수를 기반으로 새 상태 값만 계산한다.
기존 state를 수정할 수 없다. (상태 불변)
비동기 논리를 수행하거나 임의의 값을 계산하거나 다른 side effect를 일으키지 않아야 한다.
Ex.
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
// Check to see if the reducer cares about this action
if (action.type === 'counter/incremented') {
// If so, make a copy of `state`
return {
...state,
// and update the copy with the new value
value: state.value + 1
}
}
// otherwise return the existing state unchanged
return state
}
애플리케이션의 전역 상태를 보관하는 컨테이너이자 JS 객체
Redux Store 내부에 보관된 상태를 직접 수정/변경하면 안 된다.
대신, 일반 작업 객체를 만든 다음 저장소에 작업을 보내(Action) 무슨 일이 일어났는지 알려주어 상태를 업데이트할 수 있다.
그 과정은 다음과 같다.
Ex.
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
업데이트하고 해당 state를 호출/전달하는 Redux store의 기능
Ex.
store.dispatch({ type: 'counter/incremented' })
console.log(store.getState())
// {value: 1}
store의 state 값에서 특정 정보를 추출하는 함수
Ex.
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
Single Source of Truth
State is Read-Only
Changes are Make with Pure Reducer Functions
React의 공식 Redux UI 바인딩 라이브러리
npm install react-redux
React 구성 요소가 Redux store에서 데이터를 읽을 수 있게 해줌 (Redux store -> React element)
선택자를 인수로 가짐
선택자는 전체 Redux 상태를 인수로 사용하고 상태에서 일부 값을 읽고 해당 결과를 반환하는 함수
자동으로 Redux store를 구독하고 action이 생길 때마다 useSelector를 다시 실행한다.
useSelector에 의해 반환된 값이 마지막으로 실행되었을 때 변경된 경우, 새로운 참조 데이터(===)가 다시 렌더링되도록 한다.
- 배열 state인 경우 성능적인 문제가 있을 수 있다. 따라서 React.memo()를 wrapping하여 실제로 상태가 변경되었을 때만 재렌더링하거나, 배열에서 id 배열만 읽도록 하고 해당 id를 props로 전달하는 방법이 있다(* 참조).
한 Component에서 여러 개의 useSelector를 호출할 수 있다.
Ex.
import React from 'react'
import { useSelector } from 'react-redux'
import TodoListItem from './TodoListItem'
const selectTodos = state => state.todos // 선택자
const TodoList = () => {
const todos = useSelector(selectTodos)
// since `todos` is an array, we can loop over it
const renderedListItems = todos.map(todo => {
return <TodoListItem key={todo.id} todo={todo} />
})
return <ul className="todo-list">{renderedListItems}</ul>
}
export default TodoList
Ex * .
import React from 'react'
import { useSelector, shallowEqual } from 'react-redux'
import TodoListItem from './TodoListItem'
const selectTodoIds = state => state.todos.map(todo => todo.id)
const TodoList = () => {
const todoIds = useSelector(selectTodoIds, shallowEqual)
const renderedListItems = todoIds.map(todoId => {
return <TodoListItem key={todoId} id={todoId} />
})
return <ul className="todo-list">{renderedListItems}</ul>
}
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { availableColors, capitalize } from '../filters/colors'
const selectTodoById = (state, todoId) => {
return state.todos.find(todo => todo.id === todoId)
}
// Destructure `props.id`, since we only need the ID value
const TodoListItem = ({ id }) => {
// Call our `selectTodoById` with the state _and_ the ID value
const todo = useSelector(state => selectTodoById(state, id))
const { text, completed, color } = todo
const dispatch = useDispatch()
const handleCompletedChanged = () => {
dispatch({ type: 'todos/todoToggled', payload: todo.id })
}
// omit other change handlers
// omit other list item rendering logic and contents
return (
<li>
<div className="view">{/* omit other rendering output */}</div>
</li>
)
}
export default TodoListItem
Component에서 Redux store로 action을 dispatch하여 그 결과를 제공함
(Redux store <- React element)
Ex.
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
const Header = () => {
const [text, setText] = useState('')
const dispatch = useDispatch()
const handleChange = e => setText(e.target.value)
const handleKeyDown = e => {
const trimmedText = e.target.value.trim()
// If the user pressed the Enter key:
if (e.key === 'Enter' && trimmedText) {
// Dispatch the "todo added" action with this text
dispatch({ type: 'todos/todoAdded', payload: trimmedText })
// And clear out the text input
setText('')
}
}
return (
<input
type="text"
placeholder="What needs to be done?"
autoFocus={true}
value={text}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
)
}
export default Header
React-Redux hook가 올바른 Redux store를 찾는 위치와 방법으로,
Component에서 사용하려는 store를 React-Redux에 알려주어 React Component에서 store를 사용할 수 있도록 한다.
Ex.
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import App from './App'
import store from './store'
ReactDOM.render(
// Render a `<Provider>` around the entire `<App>`,
// and pass the Redux store to as a prop
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
)
https://redux.js.org/tutorials/fundamentals/part-1-overview
https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow#reducers
state, action, reducer https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers
store { enhancer , middleware } https://redux.js.org/tutorials/fundamentals/part-4-store
ui https://redux.js.org/tutorials/fundamentals/part-5-ui-react