Redux로 전역 상태 관리하기
Elice SW Track 7기 - 12주차 진행 내용입니다.
React에서 상태를 관리하는 useState hook
같은 경우, 최상위 컴포넌트에서 정의된 상태를 하위 컴포넌트에서 사용하기 위해서는 Props
로 필요한 상태를 넘겨주어야 합니다. 하지만 중첩되는 컴포넌트가 많아지고, 관리해야 하는 상태가 많아지게 되면 전역 상태를 관리하기 위한 해결책이 필요합니다. 이번에는 전역 상태 관리 라이브러리 중에서 Redux
에 대해서 알아보겠습니다.
참고 - React 상태 관리 기술 : 링크
목차
2. Flux Pattern과 useReducer hook
4. Redux toolkit을 활용한 전역 상태 관리
1. 전역 상태 관리가 필요한 이유
Props drilling
컴포넌트 구조가 복잡해지는 경우 부모 자식 컴포넌트간 깊이가 커지게 됩니다. 이 때 전역 상태 관리 기술을 사용하지 않고 Props
를 통해 상태를 넘겨준다고 하면, 최하단의 자식 컴포넌트가 데이터를 쓰기위해 최상단 컴포넌트부터 데이터를 보내야 하는 상황이 발생합니다. 이런 경우, 사용하지 않는 컴포넌트에도 Props
를 전달해줘야 하는 Props drilling
문제가 발생하게 됩니다.
데이터 캐싱과 재활용
기본적으로 하나의 HTML을 사용하는 SPA형태의 앱에서 페이지 로딩 시 마다 모든 데이터를 API요청을 통해 갱신하게 된다면 MPA(Multi Page Application)와 비교해서 속도면에서 이점이 없게 될 것입니다. 따라서 상태에 따라서 매번 새로운 데이터를 갱신해야 하는 상태, 정기적으로 갱신해야 하는 상태, 한 번 받아온 데이터를 캐시해서 활용해도 되는 상태 등을 나누는 최적화가 필요하게 됩니다. 이 때 상태 관리 기술을 사용하면 위와 같은 문제를 해결 할 수 있습니다.
2. Flux Pattern과 useReducer Hook
React에서는 기본적으로 이러한 상태 관리를 위한 useReducer hook
을 제공하고 있습니다. 별도의 라이브러리 설치를 하지 않아도 Flux pattern에 기반한 상태 관리를 구현할 수 있습니다.
Flux pattern
Flux pattern이란 Dispatcher
, Stores
, Views
(React 컴포넌트) 3가지 핵심적인 부분으로 이루어진 소프트웨어 디자인 패턴 중 하나입니다. MVC 패턴의 경우, 하나의 유저 인터렉션 발생 시 그 인터렉션으로 발생한 업데이트가 다른 연쇄 업데이트를 만들어낼 수 있기 때문에 업데이트의 근원을 추적하기 힘든 반면, Flux 패턴은 연쇄 업데이트가 아닌 단방향 업데이트만을 만들어낼 수 있습니다.
Flux 패턴의 데이터 흐름 (출처)
위 이미지에서 보이는 것 처럼 React View
에서 사용자의 인터렉션이 발생했을 때, 그 view
는 중앙의 dispatcher
를 통해 action
을 전파하게 됩니다. 어플리케이션의 데이터와 비지니스 로직을 가지고 있는 store
는 action
이 전파되면 이 action
에 영향이 있는 모든 view
를 갱신하게 됩니다.
useReducer Hook
useRedcuer Hook
이 Flux pattern에 따라서 상태 관리를 할 수 있도록 구현된 내장 Hook 입니다. 예시 코드를 통해 살펴보겠습니다.
/*
* App.js
*/
import React from 'react';
import Counter from './Counter.js';
// 초기 상태 값을 객체 형태로 선언
export const initialState = {
number: 0,
};
// state와 action 을 인자로 받는 reducer 함수를 작성
export function reducer(state, action) {
switch (action.type) {
case 'increase': {
return {
...state,
number: state.number + 1,
}
}
case 'decrease': {
return {
...state,
number: state.number - 1,
}
}
default: {
return state;
}
}
}
function App() {
return (
<div className="App">
<Counter />
</div>
)
}
/*
* Counter.js
*/
import React, { useReducer } from 'react';
import { initialState, reducer } from './App.js';
export default function Counter() {
// useReducer 메소드를 사용해서 state와 dispatch를 정의
const [state, dispatch] = useReducer(reducer, initialState);
// 상태 변경이 필요한 경우 dispatch 메소드를 호출
function handleIncreae() {
dispatch({ type: 'increase' });
}
function handleDecrease() {
dispatch({ type: 'decrease' });
}
return(
<div>
<p>{state.number}</p>
<button onClick={handleIncreae}>+</button>
<button onClick={handleDecrease}>-</button>
</div>
)
}
useReducer
메소드의 인자로 reducer
와 initialState
를 통해서 state
와 dispatch
를 선언합니다. view
에서 상태를 변경하려고 할 때 dispatch
메소드에 type
과 payload
를 담아서 호출하고, store
에 담긴 상태가 변경되고 관련된 모든 뷰가 갱신됩니다.
3. Redux
앞에서 useReducer hook
을 사용한 상태 관리 방식에 대해 살펴보았습니다. 하지만 비동기 처리나 복잡한 상태를 관리하기 위해서는 useReducer
만으로는 어렵기 때문에, 이번에는 Redux
에 대해서 알아보도록 하겠습니다.
Redux - 링크
Redux
는 useReducer와 마찬가지로 많은 개념들이 Flux pattern
차용된 상태 관리를 위한 라이브러리로, 복잡한 비동기 처리나 logger 혹은 devtool을 활용한 상태 관리를 하기 위해 사용됩니다.
Redux의 핵심 원칙 - 링크
- 단일 정보 소스 - store는 1개이며, 모든 앱의 상태가 이곳에 보관됩니다.
- 불변성 - 상태(state)는 오로지 읽을 수만 있습니다(read-only). dispatch action을 통해 새로운 상태를 생성합니다.
- 순수 함수 - 상태 변경은 어떠한 사이드 이펙트도 만들지 않아야 합니다.
action
action은 상태의 변경을 나타내는 개념 (하나의 동작만을 수행)으로 type
과 payload
를 포함하는 JS 객체 입니다.
action creator
action이 1개의 액션만을 생성한다면, action creator는 여러개의 액션을 생성하는 함수 입니다. 직접 action을 생성하는 것 보다 재사용성이 높아집니다.
store
store 는 앱 전체의 상태를 보관하는 곳입니다. action에 따라서 reducer에서는 새로운 상태를 반환하며, 다시 store에 그 결과 값을 저장합니다.
reducer
action 인자로 받아서 새로운 state 객체를 만들어 반환합니다. 상태 변경 시, 사이드 이펙트가 발생하지 않도록 함수를 설계해야 합니다.
dispatch
생성된 action 을 reducer 로 보내는 함수입니다. 이 때 필요한 미들웨어가 있다면 reducer에 도달하기 전에 사용합니다.
selector
selector는 특정 state 조각을 store로부터 가져오는 함수입니다. store의 state는 raw data를 저장하고 계산된 값 등을 selector로 가져오는 등의 패턴을 구사할 때 유용합니다.
4. Redux toolkit을 활용한 전역 상태 관리
마지막으로 Redux와 Redux toolkit을 활용한 전역 상태 관리 방법에 대해서 살펴보겠습니다. Reduc toolkit은 Redux에서 공식적으로 추천하는 helper 라이브러리입니다. 다음 명령어를 통해 설치합니다.
$ npm i -S @redux/toolkit react-redux
createSlice
/*
* counterSlice.js
*/
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
number: 0
}
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increase: state => {
state.number += 1;
},
decrease: state => {
state.number -= 1;
}
}
});
export const { increase, decrease } = counterSlice.actions;
export default counterSlice.reducer;
Redux의 reducer를 확장한 개념으로, createSlice() 를 작성하면 slice 인스턴스를 반환해주며, 해당 인스턴스로 action 과 reducer 추출이 가능하게 됩니다.
configureStore & Provider
/*
* App.js
*/
import React from 'react';
import { Provider } from 'react-redux';
import Counter from './Counter.js';
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
const store = configureStore({
reducer: counterReducer
});
function App() {
return (
<Provider store={store}>
<div className="App">
<Counter />
</div>
</Provider>
)
}
export default App;
configureStore()
를 사용하여 스토어 생성하고, react-redux 패키지에서 제공해주는 Provider
를 사용하여 생성한 store
를 바인딩합니다.
dispatch & selector
/*
* Counter.js
*/
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increase, decrease } from './counterSlice';
function Counter() {
const number = useSelector(state => state.counter.number);
const dispatch = useDispatch();
function handleIncrease() {
dispatch(increase());
}
function handleDecrease() {
dispatch(decrease());
}
return(
<div>
<p>{number}</p>
<button onClick={handleIncrease}>+</button>
<button onClick={handleDecrease}>-</button>
</div>
)
}
export default Counter;
마지막으로 store
에 정의된 상태에 접근하기 위해 useSelector
메소드를 사용하여 state
에 접근합니다. 위 예시에서는 state
의 counterSlice
에 정의된 number
값을 number
변수에 할당하여 사용하고 있습니다. (slice 생성 시 지정한 name 값을 사용)
태그 Tag : #엘리스트랙 #엘리스트랙후기 #리액트네이티브강좌 #온라인코딩부트캠프 #온라인코딩학원 #프론트엔드학원 #개발자국비지원 #개발자부트캠프 #국비지원부트캠프 #프론트엔드국비지원 #React #Styledcomponent #React Router Dom #Redux #Typescript #Javascript