1. Redux 개요 및 개념
- 애플리케이션 상태를 관리하고 업데이트하는 패턴 및 라이브러리
- 애플리케이션의 "글로벌" 상태를 관리하는데 도움을 준다.
- 상태가 많이 필요하거나 자주 업데이트되며, 상태를 업데이트하는 로직이 복잡한 경우 유용
2. Redux 용어
1) 액션(Actions)
- 타입 필드를 가진 일반 JavaScript 객체
- 타입 문자열을 "도메인/이벤트 이름" 형식으로 작성
- 액션 객체는 발생한 일에 대한 추가 정보를 포함하는 다른 필드를 가질 수 있고, 그 정보를 payload 필드에 넣는다.
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
2) 액션 생성자(Action Creators)
- 액션 객체를 생성하고 반환하는 함수
const addTodo = text => {
return (
type: 'todos/todoAdded',
payload: text
)
}
3) 리듀서(Reducers)
- 현재 상태와 액션 객체를 받아 상태를 업데이트할지 결정하고 새로운 상태를 반환하는 함수
- 로직
- 리듀서가 이 액션을 처리하는지 확인
- 그렇다면 상태를 복사하고 새로운 값으로 업데이트한 복사본을 반환
- 그렇지 않으면 변경되지 않은 기존 상태를 반환
ex)
const initState = {value:0}
function counterReducer(state=initState, action) {
//리듀서가 이 액션을 처리하는지 확인
if(action.type === 'counter/increment') {
//그렇다면, 'state'의 복사본을 확인
return {
...state,
//그리고 새 값으로 복사본을 업데이트
value: state.value+1
}
}
//그렇지 않으면 변경되지 않은 기존 상태를 반환
return state
}
4) 스토어(Store)
- 현재 Redux 애플리케이션 상태는 스토어라는 객체에 저장된다. 스토어는 리듀서를 전달받아 생성되며, getState 메서드는 현재 상태 값을 반환
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
5) 디스패치(Dispatch)
- Redux 스토어는 dispatch라는 메서드를 가지고 있다.
- 상태를 업데이트하는 유일한 방법은 store.dispatch()를 호출하고 액션 객체를 전달하는 것이다.
- 스토어는 리듀서 함수를 실행하고 새 상태 값을 내부에 저장한다.
- getState()를 호출하여 업데이트된 값을 검색할 수 있다.
store.dispatch({type:'counter/increment'})
console.log(store.getState())
//{value:1}
6) 셀렉터(Selectors)
- 스토어 상태값에서 특정 정보를 추출하는 방법을 알고 있는 함수
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
실행 단계
1) 초기 설정
- 루트 리듀서 함수를 사용하여 Redux 스토어가 생성
- 스토어는 루트 리듀서를 한 번 호출하고 반환 값을 초기 상태로 저장
- UI가 처음 렌더링될 때, UI 컴포넌트는 Redux 스토어의 현재 상태에 접근하고, 데이터를 사용하여 렌더링할 내용을 결정한다. 또한, 상태가 변경되면 이를 알 수 있도록 모든 미래의 스토어 업데이트에 구독한다.
2) 업데이트
- 애플리케이션에서 무언가 발생
- 애플리케이션 코드가 Redux 스토어에 액션을 디스패치
- 스토어는 이전 상태와 현재 액션으로 루트 리듀서 함수를 다시 실행하고, 반환 값을 새로운 상태로 저장
- 스토어는 업데이트되었음을 모든 구독된 UI 컴포넌트에 알린다.
- 각 UI 컴포넌트는 필요로 하는 상태 부분이 변경되었는지 확인
- 데이터가 변경된 것을 확인한 각 컴포넌트는 새 데이터를 사용하여 다시 렌더링
3. Redux 앱 구조
카운터 예제 앱
Redux DevTools : 초기 앱 상태
애플리케이션 구성 요소
Redux 스토어 생성
app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
- Redux 스토어는 Redux Toolkit의 configureStore 함수를 사용하여 생성된다.
- configureStore는 리듀서 인수를 전달받아야 한다.
- 애플리케이션의 각 기능은 고유한 리듀서 함수를 가질 수 있다.
- counterSlice.js 파일은 카운터 로직을 위한 리듀서 함수를 내보내는 역할을 한다. 이 함수를 여기에서 가져와 스토어를 생성할 때 포함시킬 수 있다.
Redux Slices
- "슬라이스"는 일반적으로 하나의 파일에 정의된 단일 기능에 대한 Redux 리듀서 로직 및 액션의 모음이다.
ex) 블로그 앱
- state.users, state.posts, state.comments는 각각 Redux 상태의 별도 "슬라이스"이다.
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '../features/users/usersSlice'
import postsReducer from '../features/posts/postsSlice'
import commentsReducer from '../features/comments/commentsSlice'
export default configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
}
})
슬라이스 리듀서 및 액션 생성
counterSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { fetchCount } from "./counterAPI";
const initialState = {
value: 0,
status: "idle",
};
export const incrementAsync = createAsyncThunk(
"counter/fetchCount",
async (amount) => {
const response = await fetchCount(amount);
return response.data;
}
);
export const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = "loading";
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = "idle";
state.value += action.payload;
});
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const selectCount = (state) => state.counter.value;
export const incrementIfOdd = (amount) => (dispatch, getState) => {
const currentValue = selectCount(getState());
if (currentValue % 2 === 1) {
dispatch(incrementByAmount(amount));
}
};
export default counterSlice.reducer;
- Redux Toolkit에는 createSlice라는 함수가 있어서 액션 타입 문자열, 액션 생성자 함수, 액션 객체를 생성하는 작업을 처리한다. 이 슬라이스에 대한 이름을 정의하고, 일부 리듀서 함수가 포함된 객체를 작성하면, 해당하는 액션 코드를 자동으로 생성한다.
- name 옵션의 문자열은 각 액션 타입의 첫 번째 부분으로 사용되고, 각 리듀서 함수의 키 이름은 두 번째 부분으로 사용된다.
ex) "counter" 이름 + increment"리듀서 함수는 {type: "counter/increment"} 액션 타입을 생성한다.
- createSlice는 리듀서가 호출될 때 처음 상태가 될 초기 상태 값을 전달해야 한다. 이 경우, 초기 값이 0인 value 필드를 가진 객체를 제공한다.
- createSlice는 작성한 리듀서 함수와 동일한 이름을 가진 액션 생성자를 자동으로 생성한다.
리듀서와 불변 업데이트
- Redux에서는 리듀서가 원본/현재 상태 값을 수정하는 것이 허용되지 않는다!
state.value = 123 //틀린 사용
//올바른 사용
return {
...state,
value: 123
}
- createSlice는 내부적으로 Immer라는 라이브러리를 사용한다. Immer는 Proxy라는 특수한 JS 도구를 사용하여 데이터를 래핑하고, 해당 래핑된 데이터를 "변경"하는 코드를 작성할 수 있게 한다. Immer는 시도한 모든 변경사항을 추적하고, 그 변경 목록을 사용하여 수동으로 작성한 것 처럼 안전하게 불변 업데이트된 값을 반환한다.
- Redux Toolkit의 createSlice와 createReducer에서만 "변경" 로직을 작성할 수 있다.
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
- increment 리듀서는 항상 state.value에 1을 더하고 decrement 리듀서는 1을 뺀다. Immer가 드래프트 상태 객체에 변경 사항을 감지하기 때문에 실제로 반환할 필요는 없다.
- incrementByAmount 리듀서는 카운터 값에 얼마나 더해야 하는지 알아야한다. 따라서 리듀서를 상태와 액션 인수를 모두 가지도록 선언한다.
'기타 > 글로컬청년취업사관학교' 카테고리의 다른 글
[글로컬청년취업사관학교][TIL] 240712 (0) | 2024.07.12 |
---|---|
[TIL] 240708 (0) | 2024.07.08 |
[글로컬청년취업사관학교][TIL] 240702 (0) | 2024.07.03 |
[글로컬청년취업사관학교][TIL] 240701 (0) | 2024.07.02 |
[글로컬청년취업사관학교][TIL] 240628 (1) | 2024.06.28 |