작년에 입사하고 회사에서 팀원간 스터디용으로 정리했던건데, 숙녀분께서 필요하다고 하셔서 티스토리로 두두등장 :)
Redux?
상태 관리 라이브러리중 하나로 현재까지 가장 많이 쓰이고있다. 상태관리란 UI/UX에 맞게 데이터를 관리하거나 서버와 주고받는 데이터들을 관리하는 것을 말한다.
프로젝트가 복잡하고 크기가 커질수록 상태관리의 난이도는 크게 올라간다. (이를 해결하기위해 전역으로 상태를 관리할 수 있는 라이브러리를 쓰게되는데, redux
, MobX
, recoil
등을 사용할 수 있으며 children
을 사용하는 방법도 있다.)
props drilling
리액트 특성상 단방향으로 prop
을 하위 컴포넌트로 전달하는 용도로만 쓰이게 되는데, 프로젝트가 커지다보면 위처럼 해당 데이터를 받기위해 필요없는 컴포넌트에도 어쩔 수 없이 데이터가 전달해주어야 하는 과정인 Props Drilling
이 생기게 된다. 전달이 3
5개 정도인 컴포넌트라면 문제로 생각하지 않을 수 있지만..?ㅎ 10
15개, 그 이상의 컴포넌트의 과정을 거치게 된다면 코드를 읽을 때 해당 prop
을 추적하기가 점점 힘들어진다.
Redux 정의
리덕스는 Reducer
와 Store
를 통해 상태 변경 과정을 간소화하여 기존의 문제를 해결한다. 이로 인해 복잡한 상태관리를 효율적이고 간편하게 변경하여 오류 발생률을 낮출 수 있다. 순수함수를 사용하여 상태를 예측가능하게 만들고 크롭 웹어플리케이션인 redux dev tool
을 사용해 디버깅에 유리하다.
Redux의 기본개념
- Single source of truth
- 동일한 데이터는 항상 같은곳에서 가져온다.
- 즉, 스토어라는 하나뿐인 데이터 공간이 있다는 의미이다.
- State is read-only
- 리액트에서는 setState 메소드를 활용해야만 상태변경이 가능하다.
- 리덕스에서도 액션이라는 객체를 통해서만 상태를 변경할 수 있다.
- Changes are made with pure function
- 변경은 순수 함수로만 가능하다.
- 리듀서와 연관되는 개념이다.
- Store - Action - Reducer
Redux의 기본용어
Store
- 스토어는 컴포넌트의 상태를 관리하는 저장소다. 하나의 프로젝트는 하나의 스토어만 가질 수 있다.
- 컴포넌트에서 상태 정보가 필요할 때 스토어에 접근한 후 스토어 안에서 앱에 필요한 상태를 담는다.
Action
- 스토어의 상태를 변경하기 위해선 액션을 생성해야한다. 앱에서 스토어에 운반할 데이터를 지칭한다.
- 액션은 자바스크립트 객체형태이며, 반드시 타입을 가져야한다. 액션 객체는 액션생성 함수에 의해 만들어진다.
Reducer
- 리듀서는 현재 상태와 액션 객체를 받아 새로운 상태를 리턴하는 함수다.
- 액션을 스토어에 바로 전달하는 것이 아닌 리듀서에 전달 후 리듀서가 주문을 보고 스토어의 상태를 업데이트 한다.
- 액션을 리듀서에 전달하기 위해서는 디스패치 메소드를 사용한다.
Dispatch
- 디스패치는 스토어의 내장 함수 중 하나이며, 액션 객체를 넘겨 상태를 업데이트 시켜주는 역할을 한다.
Subscribe
- 스토어의 내장함수 중 하나로 리듀서가 호출될 때
Subscribe
된 함수 및 객체를 호출한다.
- UI가 처음 렌더링 될때, UI 컴포넌트는 리덕스 스토어의 상태에 접근하여 해당 상태를 렌더링한다.
- 이후 UI에서 상태가 변경되면, 앱은 디스패치를 실행해 액션을 일으킨.
- 새로운 액션을 받은 스토어는 리듀서를 실행하고 리듀서를 통해 나온 겂을 새로운 상태로 저장한다.
- 서브스크라이브된 UI는 상태 업데이트로 변경된 데이터를 새롭게 렌더링한다.
Redux Toolkit (RTK)
장점이 가득해 보이지만, 리덕스 또한 단점이 존재한다.
- 스토어 환경 설정이 너무 복잡하다.
- 유용하게 사용하기 위해서는 많은 패키지를 추가해줘야한다.
- 보일러플레이트, 즉 어떤일을 하기위해 꼭 작성해야 하는 상용구코드를 너무 많이 요구한다.
이러한 이슈를 해결하기위해 RTK
가 등장하게된다. 공식 문서는 이렇게 설명한다.
Redux Toolkit은 효율적인 Redux 개발을 위한 저희의 견해를 반영한, 이것만으로도 작동하는 도구 모음입니다. Redux Toolkit은 Redux 로직을 작성하기 위한 표준 방식이 되도록 만들어졌고, 사용하기를 강력히 추천드립니다.
리덕스 로직을 작성하는 표준 방식이 되기위한 의도로 만들어 진 RTK의 핵심은 기존 리덕스의 복잡성을 낮추고 사용성을 높이는 것이다. 리액트가 CRA(Create React App)
을 통해 개발 접근성을 높였듯 RTK
도 복잡한 리덕스 설정과정을 포함해 유스케이스 전반에 걸쳐 추상화를 시도했다. 툴킷에서 제공하는 함수는 사용자에게 애플리케이션 코드를 간단히 작성할 수 있도록 지원하는데 집중한다.
RTK 주요 API
리덕스 코어 라이브러리의 표준 함수인 createStore
를 추상화한 것으로 스토어를 구성하는 함수다. 기존 리덕스의 번거로운 기본 설정 과정을 자동화 한다.
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./reducers";
const store = configureStore({ reducer: rootReducer });
// The store now has redux-thunk added and the Redux DevTools Extension is turned on
createStore를 감싸서 쓸만한 기본값들과 단순화된 설정을 제공합니다. 여러가지 리듀서 조각들을 자동으로 합쳐주고, 기본 제공되는 redux-thunk를 포함해서 여러분이 지정한 미들웨어들을 더해주고, Redux DevTools 확장을 사용할 수 있게 합니다.
configureStroe
함수는 reducer
, middleware
, devTools
, preloadedState
, enhancers
정보를 전달한다.
reducer
: 리듀서에는 단일 함수를 전달하여 스토어의 루트 리듀서(root reducer)로 바로 사용한다. 또한 슬라이스 리듀서들로 구성된 객체를 전달하여 루트 리듀서를 생성하도록 할 수 있다. 이런 경우에는 내부적으로 기존 리덕스combineReducers
함수를 사용해 자동병합하여 루트 리듀서를 생성한다.middleware
: 기본적으로는 리덕스 미들웨어를 담는 배열이다. 사용할 모든 미들웨어를 배열에 담아서 명시적으로 작성할 수도 있으며, 다른 방법으로는getDefaultMiddleware
를 호출한다. 사용자 정의, 커스텀 미들웨어를 추가하면서 동시에 리덕스 기본 미들웨어를 사용할 때 유용하다.devTools
: 불리언값으로 리덕스 개발자 도구를 끄거나 켠다.preloadedState
: 스토어의 초기값을 설정할 수 있다.enhaners
: 기본적으로는 배열이지만 콜백 함수로 정의하기도 한다. 예를 들어 다음과 같이 작성하면 개발자가 원하는 store enhancer를 미들웨어가 적용되는 순서보다 앞서서 추가할 수 있다.
리덕스 미들웨어는 dispatch(이하 디스패치)된 액션이 리듀서에 도달하기 전 중간 영역에서 사용자의 목적에 맞게 기능을 확장할 수 있도록 돕는다. 상태의 변형(mutation)을 감지하거나 직렬화, 즉 데이터를 다른 데이터 구조로 맞추어 가공하는 행위가 불가능한 값(non-serializable value)을 사용하는 실수를 방지할 수 있도록 경고해준다. 여기서 직렬화가 불가능하다는 의미는 어떤 데이터를 직렬화 하는 과정에서 데이터를 유실할 수도 있다는 것을 의미한다.
직렬화가 불가능한 값을 액션이나 상태에서 사용하지 않는 것을 권장하고있으며, 직렬화가 불가능한 값들은 Promise, Symbol, Map/Set, function, class instance 등이 있다. 반면에 직렬화가 가능한 값들은 자바스크립트 원시 자료형에 속하는 string, number, null, undefined와 array, object literal(객체 리터럴) 방식으로 선언된 plain object가 있다.
const store = configureStore({
...
enhancers: (defaultEnhancers) => [reduxBatch, ...defaultEnhancers],
})
// [reduxBatch, applyMiddleware, devToolsExtension]
상태에 변화를 일으키는 리듀서 함수를 생성하는 유틸함수다. 내부적으로 immer
라이브러리를 사용하여 mutative한 코드, 예컨대 state.todos[3].completed = true
형태로 작성해도 불변(immutable) 업데이트가 이루어지도록 로직을 간단히 할 수 있습니다.
// 기존 스위치 문으로 이루어진 카운터 리듀서 함수입니다.
// 많은 보일러플레이트 코드와 에러를 발생시키기 쉬운 구조를 보여주고 있습니다.
function todosReducer(state = [], action) {
switch (action.type) {
case "UPDATE_VALUE": {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue,
},
},
},
};
}
default: {
return state;
}
}
}
// 하지만 createReducer 함수를 사용하면 아래처럼 간단히 작성할 수 있습니다.
const todosReducer = createReducer((state = []), (builder) => {
builder.addCase("UPDATE_VALUE", (state, action) => {
const { someId, someValue } = action.payload;
state.first.second[someId].fourth = someValue;
});
});
RTK
에서 case reducer(이하 케이스 리듀서)
가 액션을 처리하는 두 가지 방법은 builder callback
표기법과 map object
표기법이 있다. 두 방법 모두 동일한 역할을 하지만 타입스크립트와의 호환성을 위해서는 builder callback
표기법이 더 선호된다. (자바스크립트는 map object
표기법 선호)
builder.addCase(actionCreator, reducer)
: 액션 타입과 맵핑되는 케이스 리듀서를 추가하여 액션을 처리한다.addMatcher
또는addDefaultCase
메서드 보다 먼저 작성되어야 합니다.builder.addMatcher(matcher, reducer)
: 새로 들어오는 모든 액션에 대해서 주어진 패턴과 일치하는지 확인하고 리듀서를 실행한다.builder.addDefaultCase(reducer)
: 그 어떤 케이스 리듀서나 매처 리듀서도 실행되지 않았다면, 기본 케이스 리듀서가 실행된다.
const counterReducer = createReducer(0, {
increment: (state, action) => state + action.payload,
decrement: (state, action) => state - action.payload,
});
// 위 예제처럼 작성하거나
// 또는 'createAction'에서 생성된 액션 생성자(action creator)를
// 연산된 프로퍼티(computed property) 문법을 사용해서 바로 '키'로 사용할 수 있습니다.
const increment = createAction("increment");
const decrement = createAction("decrement");
const counterReducer = createReducer(0, {
[increment]: (state, action) => state + action.payload,
[decrement.type]: (state, action) => state - action.payload,
});
createReducer(initialState, actionsMap, actionMatchers, defaultCaseReducer)
initialState
: 리듀서가 최초로 호출되었을 때 사용될 상태 값입니다.actionsMap
: 액션 타입이 케이스 리듀서에 맵핑되어 있는 객체입니다.actionMatchers
: { matcher, reducer } 형태로 정의된 매처를 배열로 담습니다. 매칭된 리듀서는 순서대로 독립적으로 실행됩니다.defaultCaseReducer
: 그 어떤 케이스 리듀서나 매처 리듀서도 실행되지 않았다면, 기본 케이스 리듀서가 실행됩니다
// matcher
const isStringPayloadAction = (action) => typeof action.payload === 'string'
const lengthOfAllStringsReducer = createReducer(
// initialState
{ strLen: 0, nonStringActions: 0 },
// actionsMap
{
/* [...]: (state, action) => {} */
},
// actionMatchers
[
{
matcher: isStringPayloadAction,
reducer(state, action) {
state.strLen += action.payload.length
},
},
],
// defaultCaseReducer
(state) => {
state.nonStringActions++
}
기존 리덕스 코어 라이브러리에서 액션을 정의하는 일반적인 접근법은 액션 타입 상수
와 액션 생성자 함수
를 분리하여 선언했으나 RTK
두 과정을 createAction
함수를 사용해 하나로 결합하여 추상화했다.
// BEFORE
const INCREMENT = "counter/increment";
function increment(amount: number) {
return {
type: INCREMENT,
payload: amount,
};
}
const action = increment(3);
// { type: 'counter/increment', payload: 3 }
// AFTER
import { createAction } from "@reduxjs/toolkit";
const increment = createAction("counter/increment");
const action = increment(3);
// { type: 'counter/increment', payload: 3 }
// toString() 메서드를 오버라이딩하여 action creator (이하 액션 생성자) 객체를 액션 타입 문자열로 표현하여 액션 생성자를 직접 키로 사용
const increment = createAction("counter/increment");
const counterReducer = createReducer(0, {
[increment]: (state, action) => state + action.payload,
[decrement.type]: (state, action) => state - action.payload,
});
console.log(increment.toString());
// 'counter/increment'
console.log(`The action type is: ${increment}`);
// 'The action type is: counter/increment'
이름과 상태 초기값, 리듀서 함수들로 이루어진 객체를 받아 그에 맞는 액션 생산자와 액션 타입을 포함하는 리듀서 조각을 자동으로 만들어줍니다.
const alertSlice = createSlice({
name: "todos",
initialState,
reducers: {},
extraReducers: (builder) => {},
});
리덕스 로직을 작성하는 표준 접근법은 createSlice
를 사용하는 것에서 출발합니다.
앞서 소개해드린 createAction
, createReducer
함수가 내부적으로 사용되며 createSlice
에 선언된 슬라이스 이름을 따라서 리듀서와 그리고 그것에 상응하는 액션 생성자와 액션 타입을 자동으로 생성합니다. 따라서 createSlice
를 사용하면 따로 createAction
, createReducer
를 작성할 필요가 없습니다.
나는 RTK사용할때 createSlice 사용하고있다.
액션 타입 문자열과 프로미스를 반환하는 함수를 받아, pending/fulfilled/rejected
액션 타입을 디스패치해주는 thunk를 생성해줍니다. createAction
의 비동기 버전을 위해서 제안되었습니다. 액션 타입 문자열과 프로미스를 반환하는 콜백 함수를 인자로 받아서 주어진 액션 타입을 접두어로 사용하는 프로미스 생명 주기 기반의 액션 타입을 생성합니다.
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId: number, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
interface UsersState {
entities: []
loading: 'idle' | 'pending' | 'succeeded' | 'failed'
}
const initialState = {
entities: [],
loading: 'idle',
} as UsersState
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
builder.addCase(fetchUserById.fulfilled, (state, action) => {
state.entities.push(action.payload)
})
},
})
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
리덕스 스토어 상태에서 데이터를 추출할 수 있도록 도와주는 유틸리티. selector에 메모이제이션을 더해 렌더링 최적화에 기여할 수 있는 기능을 제공한다.createSelector
는 Reselect
라이브러리를 기반으로 하고, Reselect
는 기본적으로 메모이제이션을 제공한다. 때문에 createSelector
를 이용해 새로운 값을 반환하면 해당 값은 다시 리렌더링 되더라도 연산을 새로 수행하는 대신 이전에 캐싱해두었던 값을 반환하게 되는 것이다.
const selectSelf = (state: State) => state;
const unsafeSelector = createSelector(selectSelf, (state) => state.value);
const draftSafeSelector = createDraftSafeSelector(
selectSelf,
(state) => state.value
);
// in your reducer:
state.value = 1;
const unsafe1 = unsafeSelector(state);
const safe1 = draftSafeSelector(state);
state.value = 2;
const unsafe2 = unsafeSelector(state);
const safe2 = draftSafeSelector(state);
저장소 내에 정규화된 데이터를 다루기 위한 리듀서와 셀렉터를 만들어줍니다. 정규화된 상태 구조, 즉 중복을 취소화하기 위해서 데이터가 구조화되고, 일관성이 보장된 구조에서 효율적인 CRUD를 수행하기 위해 미리 빌드된 리듀서 및 셀렉터를 생성하는 함수입니다. CRUD 함수를 따로 제공하고 있습니다.
추가로, Redux Toolkit
에는 새로운 RTK Query 데이터 패치 API
도 포함되어 있습니다. RTK Query
는 Redux에서 데이터를 가져오고 캐싱하기 위한 강력한 도구입니다. 이 API는 웹 애플리케이션에서 데이터를 불러오는 일반적인 경우에 패치와 캐시 로직을 직접 작성해야 할 필요를 없애줍니다.
정리
- 리덕스는 데이터를 단방향으로 흐르게 하여 결과를 예측 가능하게 하고 디버깅을 쉽게 만든다.
- RTK는 기존 리덕스의 문제를 개선하고, 리덕스 로직을 작성하는 표준을 제안한다.
- 스토어 구성은
configureStore
를 사용하자. - 리덕스 로직을 작성할 때는 덕-패턴(ducks pattern) 형태로 작성을 돕는
createSlice
를 사용하자. - 비동기 로직은
createAsyncThunk
를 사용해서 작성하자.- 타입 스크립트는 지원하는 빌더 콜백(builder callback) 표기법을 사용한다.
- 프로미스 생명 주기를 따르는 액션 타입으로 비동기 로직을 관리하자.
- 데이터 패칭과 캐싱은
RTK Query
를 사용해 보자.
- 스토어 구성은
더 자세히 알고싶다면
'개발 > Javascript' 카테고리의 다른 글
[msw] msw 2.x.x 리액트에서 사용하기 (0) | 2024.06.11 |
---|---|
[Javascript] React에 Zustand와 TanStack Query 함께 사용하기 (3) | 2024.03.24 |
[자스 딥다이브 스터디] 04장 변수 (4) | 2024.02.22 |
[Javascript] p5js로 해머게임 만들기 (빵형의 개발도상국) (4) | 2024.01.21 |
[Javascript] history.pushState() (2) | 2024.01.02 |