Redux는 action이라고 불리는 이벤트를 사용하여 애플리케이션 상태를 관리하고 업데이트하기 위한 패턴이자 라이브러리이다 . 애플리케이션 전체에서 필요로 하는 상태를 중앙에서 저장하고, 상태가 예측 가능한 방식으로 업데이트될 수 있도록 규칙을 제공한다.
Redux는 애플리케이션의 여러 부분에서 필요한 전역상태를 관리하는 데 도움을 준다.
Redux는 공유 상태 관리를 처리하는 데 도움이 되지만, 단점도 있다. 학습해야 할 개념이 많고 작성해야 할 코드가 더 많다. 또한 코드에 약간의 간접성을 추가하며, 특정 규칙을 따르도록 요구한다.
Redux가 더 유용한 경우는 다음과 같다:
React-Redux:
Redux는 자주 React와 함께 사용된다. React-Redux는 Redux 스토어와 상호작용하여 상태를 읽고 스토어를 업데이트하는 액션을 디스패치할 수 있게 해주는 공식 패키지이다.
Redux Toolkit
Redux Toolkit은 Redux 로직을 작성하는 데 권장되는 접근 방식이다. Redux 애플리케이션을 구축하는 데 필수적인 패키지와 기능을 포함하고 있다. Redux Toolkit은 대부분의 Redux 작업을 간소화하고 일반적인 실수를 방지하며 Redux 애플리케이션을 쉽게 작성할 수 있도록 돕는다.
Redux DevTools Extension
Redux DevTools Extension은 Redux 스토어의 상태 변화 기록을 시간 순으로 보여줍니다. 이를 통해 애플리케이션을 효과적으로 디버깅할 수 있으며, "타임 트래블 디버깅"과 같은 강력한 디버깅 기법도 사용할 수 있습니다.
다양한 컴포넌트에서 상태가 공유되지 않는 작은 프로젝트에서 리덕스를 사용할 필요는 없다. 하지만 다른 곳에 위치하는 다양한 컴포넌트들이 똑같은 상태를 참조하고 변화시킨다면 코드는 매우 복잡해질 뿐더러, 원하지 않게 상태를 변화시키는 실수를 범할 가능성이 높아진다. 이때 리덕스는 빛을 발한다.
상태를 공유하는 컴포넌트를 부모 컴포넌트에 끌어올림으로써 어느정도 문제를 해결할 수 있지만, 이는 props drilling으로 이어져 코드를 더욱 복잡해보이게할 뿐만 아니라 부모 컴포넌트가 자식 컴포넌트에 종속적이게 될 수밖에 없기 때문에 유지/보수에 안 좋을 수밖에 없다.
따라서 리덕스는 공유 상태를 컴포넌트에서 분리하여, 컴포넌트 트리의 외부에 위치시킴으로써 독립적으로 관리하는 접근법을 사용한다. 이를 통해 컴포넌트 트리는 하나의 큰 뷰가 되어 트리 내 어디에 있든 상관없이 모든 컴포넌트가 상태에 접근하거나 액션을 트리거할 수 있게 된다.
이것이 Redux의 기본 아이디어이다. 애플리케이션의 전역 상태를 컴포넌트 트리와 분리된 하나의 위치에 모아두고, 상태를 업데이트할 때 코드가 예측 가능하도록 특정 패턴을 따르게 하는 방식인 것이다.
Redux는 모든 상태 업데이트가 불변하게(immutably) 수행되기를 기대한다.이는 변화의 예측 가능성과 안정성을 보장하기 위해서이다. 상태가 불변하게 유지되면, 이전 상태와 새로운 상태를 비교하는 것이 쉬워지며, 상태 변경의 추적이 명확해진다.
JavaScript 객체와 배열은 기본적으로 mutable(변경 가능)하다. 객체를 생성하면 해당 속성의 내용을 변경할 수 있고, 배열을 생성하면 그 내용을 변경할 수 있다.
const obj = { a: 1, b: 2 }
// 여전히 동일한 객체이지만, 내용이 변경되었습니다.
obj.b = 3
const arr = ['a', 'b']
// 마찬가지로, 이 배열의 내용을 변경할 수 있습니다.
arr.push('c')
arr[1] = 'd'
메모리 내에서 동일한 객체나 배열을 참조하고 있지만, 객체 내부의 내용은 변경된 것이다.
값을 불변(immutable)하게 업데이트하려면, 기존 객체나 배열을 복사한 후 복사본을 수정해야 한다.
이 작업은 JavaScript의 배열,객체의 스프레드 연산자와 원본 배열을 변경하지 않고 새로운 배열을 반환하는 배열 메서드를 사용하여 수동으로 수행할 수 있다.
const obj = {
a: {
// obj.a.c를 안전하게 업데이트하려면 각 부분을 복사해야 합니다.
c: 3
},
b: 2
}
const obj2 = {
// obj를 복사합니다.
...obj,
// a를 덮어씁니다.
a: {
// obj.a를 복사합니다.
...obj.a,
// c를 덮어씁니다.
c: 42
}
}
const arr = ['a', 'b']
// 새로운 배열을 생성하고 "c"를 끝에 추가합니다.
const arr2 = arr.concat('c')
// 또는, 원본 배열의 복사본을 만들 수 있습니다.
const arr3 = arr.slice()
// 그리고 복사본을 변경합니다.
arr3.push('c')
액션은 type 필드를 가진 객체로써 애플리케이션에서 발생한 일을 설명하는 이벤트이다.
type 필드는 액션에 설명적인 이름을 부여하는 string이어야 하며, 예를 들어 "todos/todoAdded"와 같은 형식이다. 일반적으로 type 문자열을 도메인/이벤트이름 형식으로 작성하며, 첫 부분은 액션이 속하는 기능이나 카테고리를, 두 번째 부분은 구체적인 이벤트를 나타낸다.
액션은 발생한 일에 대한 추가 정보를 담는 다른 필드를 가질 수도 있다. 관례상, 이 정보를 payload라는 필드에 넣는다.
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
액션 생성자는 액션 객체를 생성하고 반환하는 함수이다. 위의 예시처럼 액션 객체를 직접 작성하기 보다 이 생성자를 통해 작성하는 경우가 일반적이다.
const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}
이렇게 함으로써 액션 객체를 일관되게 생성할 수 있으며, 코드의 유지보수성과 재사용성이 향상된다.
리듀서는 현재 상태와 액션 객체를 받아 상태를 업데이트할지 결정하고, 새로운 상태를 반환하는 함수이다. (state, action) => newState. 즉, 리듀서는 인수로 받은 액션(이벤트) 타입에 따라 이벤트를 처리하는 이벤트 리스너라고 생각하면 된다.
리듀서는 다음과 같은 특정 규칙을 항상 따라야 한다:
💡state와 action 인수를 기반으로 새 상태 값을 계산해야 한다.다음은 리듀서가 따라야 할 단계를 보여주는 간단한 리듀서 예제이다:
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
// 리듀서가 이 액션을 처리해야 하는지 확인합니다.
if (action.type === 'counter/increment') {
// 그렇다면 `state`를 복사합니다.
return {
...state,
// 복사본을 새로운 값으로 업데이트합니다.
value: state.value + 1
}
}
// 그렇지 않으면 기존 상태를 변경하지 않고 반환합니다.
return state
}
리듀서는 if/else, switch, 반복문 등 모든 종류의 로직을 사용하여 새 상태가 무엇이 되어야 하는지를 결정할 수 있다.
Redux가 관리하는 상태들는 store라는 객체에 저장된다.
스토어는 reducer를 전달하여 생성되며, 현재 상태 값을 반환하는 getState라는 메서드를 가지고 있다:
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
이렇게 하면 store 객체를 통해 애플리케이션의 상태를 관리하고 접근할 수 있습니다.
Redux 스토어에는 dispatch라는 메서드가 있다. 상태를 업데이트하는 유일한 방법은 store.dispatch()를 호출하고 액션 객체를 전달하는 것이다. 그러면 스토어는 리듀서 함수를 실행하고 새 상태 값을 저장하며, getState()를 호출하여 업데이트된 값을 가져올 수 있다:
store.dispatch({ type: 'counter/increment' })
console.log(store.getState())
// {value: 1}
액션을 디스패치하는 것을 애플리케이션에서 "이벤트를 발생시키는 것"으로 생각하면 된다. 즉 디스패치를 통해 매개변수로 전달된 리듀서가 이벤트 리스너처럼 작동하여, 상태가 업데이트 되는 것이다.
일반적으로 액션 생성자를 호출하여 올바른 액션을 디스패치한다:
const increment = () => {
return {
type: 'counter/increment'
}
}
store.dispatch(increment())
console.log(store.getState())
// {value: 2}
이렇게 하면 액션을 일관되게 디스패치할 수 있어 코드의 유지 보수성을 높일 수 있다.
선택자는 스토어 상태 값에서 특정 정보를 추출하는 함수이다. 애플리케이션이 커지면, 애플리케이션의 다양한 부분이 동일한 데이터를 읽어야 할 때 반복되는 로직을 피하는 데 도움이 된다.
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
이렇게 선택자를 사용하면 상태에서 특정 데이터를 일관되게 추출할 수 있어 코드의 가독성과 유지 보수성을 향상시킬 수 있습니다.
단방향 데이터 흐름은 애플리케이션을 업데이트하는 일련의 단계이다:
Redux에서 이 단계를 좀 더 구체적으로 나눌 수 있다:
import { configureStore } from '@reduxjs/toolkit'
// 초기 상태를 정의합니다.
const initialState = { value: 0 }
// 루트 리듀서를 정의합니다.
function rootReducer(state = initialState, action) {
switch (action.type) {
case 'counter/increment':
return { ...state, value: state.value + 1 }
default:
return state
}
}
// configureStore 함수를 사용하여 스토어를 생성합니다.
const store = configureStore({
reducer: rootReducer
})
console.log(store.getState())
// { value: 0 }
// 컴포넌트 정의
function Counter() {
// useDispatch 훅을 통해 디스패치 함수 가져오기
const dispatch = useDispatch()
// useSelector 훅을 통해 현재 상태 값 선택
const count = useSelector((state) => state.value)
// 버튼 클릭 시 'counter/increment' 액션을 디스패치
const handleIncrement = () => {
dispatch({ type: 'counter/increment' })
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
</div>
)
}
dispatch({type: 'counter/increment'}) 가 디스패치이다.애플리케이션 내에서 무언가가 발생하면:
Redux는 여러 유형의 코드를 사용합니다:
type 필드를 가진 단순 객체로, 애플리케이션에서 "무슨 일이 일어났는지"를 설명한다.