리덕스는 상태 관리 라이브러리입니다. 리덕스를 사용하면 애플리케이션의 전체 상태를 관리할 수가 있습니다. 물론 리덕스 같은 라이브러리를 사용하지 않아도 리액트 만으로도 애플리케이션을 만들 수 있기는 합니다. 작은 규모의 프로젝트라면 리액트만 사용해도 문제가 없습니다. 하지만 규모가 큰 프로젝트라면 리덕스를 사용하는 것이 좋습니다.
리덕스를 사용했을 때의 장점은 아래와 같습니다.
1. 컴포넌트 코드로부터 상태 관리 코드를 분리할 수 있습니다.
2. 미들웨어를 활용한 다양한 기능을 추가할 수 있습니다.
- 강력한 미들웨어 라이브러리 (redux-saga)
- 로컬 스토리지에 데이터 저장하기 및 불러오기
3. SSR(서버 사이드 렌더링) 시 데이터 전달이 간편합니다.
4. 리액트 컨텍스트보다 효율적인 렌더링이 가능합니다.
리덕스를 사용하면 컴포넌트 코드로부터 상태 관리 코드를 분리할 수 있습니다. 아무래도 컴포넌트에서 상당히 많은 코드를 작성하게 되는데, 그러다보면 컴포넌트 코드가 많아져서 가독성이 떨어지는 경우가 있습니다. 그래서 상태 관리 코드를 분리해주면 컴포넌트 코드가 좀 더 가벼워집니다.
그리고 리덕스에는 미들웨어라는 개념이 있습니다. 데이터를 처리하는 중간과정에 로직을 넣어서 필요한 기능을 추가할 수 있습니다. 리덕스 생태계에는 강력한 미들웨어 라이브러리가 많이 있습니다. 예를들면 redux-saga와 같은 것이 있습니다. 그리고 미들웨어에서 로컬 스토리지에 데이터를 저장하거나 불러오는 기능을 매우 간단하게 구현할 수가 있습니다.
그리고 또다른 장점은 서버사이드 렌더링 시, 데이터 전달이 간편하다는 겁니다. 리덕스의 상태값은 하나의 객체로 표현이 가능합니다. 따라서 그 하나의 객체만 문자열로 변환해서 서버에서 클라이언트로 전달하면 되기 때문에 매우 간편합니다. 그리고 클라이언트는 받은 문자열을 하나의 객체로 만들어서 사용하면 됩니다. 이렇게 하나의 객체로 관리가 되기 때문에 클라이언트에서 전체 애플리케이션의 상태를 저장해서 불러오는 이런 기능도 간단하게 구현 할 수 있고, 과거의 상태를 저장했다가 과거의 상태로 돌아가는 것도 간단히 구현이 가능합니다.
리액트 Context API를 사용하면 되지 않냐고 생각하시는 분들도 있을겁니다. 물론 그렇게 사용해도 됩니다. 하지만 리덕스를 사용하면 좀더 효율적인 렌더링이 가능합니다.
우선 Context API로 상태관리를 하는 경우를 작성해 보겠습니다.
루트 컴포넌트(App) - Context API
최상위 컴포넌트인 App 컴포넌트에서 상태 및 상태관리 함수를 담당할 컨텍스트를 만들고 Provider로 감싸줍니다.
import React, { userContext, createContext, useReducer } from 'react';
// 컨텍스트
const AppContext = createContext({});
const DispatchContext = createContext(() => {});
// 루트 컴포넌트
export default function App() {
const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
return(
<>
<AppContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
<User />
<Product />
</DispatchContext.Provider>
</AppContext.Provider>
</>
);
}
// 리듀서
const INITIAL_STATE = {
user: {name: 'horong'},
produce: {name: 'Galaxy S20'},
}
function reducer(state, action) {
switch (action.type) {
case 'setUserName':
return {
...state,
user: {...state.user, name: action.name}}
}
}
리듀서(reducer) - Context API
여러개의 상태값을 한번에 관리하기 위해서는 useReducer 훅을 사용하는 것이 유리하기 때문에 아래와 같이 리듀서를 만들어줍니다.
// 리듀서
const INITIAL_STATE = {
user: {name: 'horong'},
produce: {name: 'Galaxy S20'},
}
function reducer(state, action) {
switch (action.type) {
case 'setUserName':
return {
...state,
user: {...state.user, name: action.name}
};
default:
return state;
}
}
하위 컴포넌트(User, Product) - Context API
렌더링할 컴포넌트를 만들어 줍니다. 컴포넌트 내부에서 App 컴포넌트에서 내려주고있는 상태값과 상태값 변경 함수를 사용하기 위해 useContext 훅을 사용합니다.
// User 컴포넌트
function User() {
const { user } = useContext(AppContext);
const dispatch = useContext(DispatchContext);
return (
<>
<p>{user.name}</p>
<button onClick={() => dispatch({ type: 'setUserName', name: 'hong' })}>
사용자 이름 수정
</button>
</>
)
}
// Product 컴포넌트
function Product() {
const { product } = useContext(AppContext);
return <p>{`제품 이름: ${product.name}`}</p>
}
잘 보시면 상태값을 하나의 context로 관리하고 있습니다. 이러한 경우에 상태값이 하나만 바뀌어도 전체가 다시 렌더링 되는 현상이 발생합니다. 리덕스에서도 이렇게 하나의 객체로 관리를 하고 있긴 하지만, 렌더링이 필요한 컴포넌트만 되도록 할 수가 있습니다. 따라서 리덕스는 context API를 사용했을 경우에 상태값을 작은 영역으로 나누어서 많은 context로 관리해야하는 수고를 덜어줄 수 있습니다. 그리고 context API를 사용하는 경우에는 userContext / productContext 로 나눈다고 하더라도 데이터를 동적으로 다루어야 하는 경우에는 까다롭습니다. 이번에는 위의 코드를 리덕스로 작성해 보겠습니다.
루트 컴포넌트(App) - Redux
App 컴포넌트에서 바뀐 부분은 Provider가 하나로 통합되었습니다. 리듀서 함수를 사용하여 store 라는 객체를 만들고 Provider 컴포넌트의 속성값으로 내려주어서 상태를 관리할 수 있도록 해주고 있습니다.
import React from 'react';
import { createStore } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';
import rootReducer from './rootReducer'
// 루트 컴포넌트
export default function App() {
return (
<>
<Provider store={store}>
<User />
<Product />
</Provider>
</>
);
}
// 리듀서
const store = createStore(rootReducer);
리듀서(reducer) - Redux
리듀서의 코드는 아래와 같습니다. 변경된 부분은 초기화 값을 state 인자에 지정해준 부분 밖에 변경된 것이 없습니다.
// 리듀서
const INITIAL_STATE = {
user: {name: 'horong'},
produce: {name: 'Galaxy S20'},
}
function reducer(state = INITIAL_STATE, action) {
switch (action.type) {
case 'setUserName':
return {
...state,
user: {...state.user, name: action.name}
};
default:
return state;
}
}
하위 컴포넌트(User, Product) - Context API
useContext 를 사용하던 부분을 상태값은 useSelector를 사용하고 상태값 변경함수는 useDispatch를 사용하였습니다.
// User 컴포넌트
function User() {
const user = useSelector(state => state.user);
const dispatch = useDispatch();
return (
<>
<p>{user.name}</p>
<button onClick={() => dispatch({ type: 'setUserName', name: 'hong' })}>
사용자 이름 수정
</button>
</>
);
}
// Product 컴포넌트
function Product() {
const product = useSelector(state => state.product);
return <p>{`제품 이름: ${product.name}`}</p>
}
비교를 해 보시면 큰 차이는 없는걸 확인할 수 있습니다. 하지만 리덕스에서는 아까 언급했던 것과 같이 Context API를 사용하여 UserContext / ProductContex 로 나누지 않아도 렌더링이 효율적으로 동작하는 것을 확인하실 수 있습니다.
출처 : 실전 리액트 프로그리맹 - 인프런 (추천합니다 !!)
[Frontend/React] 23. 리덕스의 구조 - 리듀서, 스토어 (0) | 2020.10.06 |
---|---|
[Frontend/React] 22. 리덕스의 구조 - 액션, 미들웨어 (0) | 2020.10.06 |
[Frontend/React] 20. 렌더링 속도를 올리기 위한 성능 최적화 방법 (0) | 2020.10.06 |
[Frontend/React] 19. useEffect 활용법 (0) | 2020.10.05 |
[Frontend/React] 18. 재사용성을 고려한 컴포넌트의 분리 (0) | 2020.10.05 |