Redux란 컴포넌트와 상태를 분리하여 전역에서 상태 관리를 해줄 수 있게 해주는 상태 관리 라이브러리이다. React 애플리케이션을 개발할 때 Redux를 사용하면 React 컴포넌트 간의 복잡한 데이터 흐름을 따라갈 필요가 없어진다.
그렇다고 웹 애플리케이션 개발 시 상태 관리 라이브러리가 꼭 필요한건 아니다. Redux를 사용하지 않아도 충분히 웹 애플리케이션을 만들 수 있다. 하지만 컴포넌트의 구조가 복잡해질수록 상태 관리가 힘들어지고 props drilling이 발생하게 되므로 상태 관리 라이브러리를 사용하는 것이 효율적이다.
*로컬 상태 - 특정 컴포넌트 내에서만 관리되는 상태
*전역 상태 - 다른 컴포넌트와 상태를 공유하고 영향을 끼치는 상태
Props Drilling은 상위 컴포넌트의 state를 props를 통해 하위 컴포넌트로 전달하는 과정을 의미하는데 상위 컴포넌트와 하위 컴포넌트 사이에 props를 전달하는 용도로만 쓰이는 컴포넌트가 존재한다.
Component A -> Component B -> Component C -> Component D -> Component E
A 컴포넌트에서 E 컴포넌트로 props를 전달할 때 B, C, D 컴포넌트들을 거쳐야한다.
이때 A 컴포넌트의 state를 E 컴포넌트에서 변경했을 때 모든 컴포넌트에서 re-redering이 발생한다.
최상위 컴포넌트인 A컴포넌트에서 state를 관리하고 state가 변경될 때마다 re-rendering 되면서 모든 컴포넌트가 re-rendering된다. 변경되는 상태와 연관없는 컴포넌트 까지 불필요하게 re-rendering되는 것이다.
props의 전달 횟수가 5회 이내라면 Props Drilling은 크게 문제가 되지 않지만 프로젝트 규모가 커지고 구조가 복잡해질수록 props의 전달 과정이 늘어나기 때문에 문제점들이 발생한다.
해결 방법은 상태 관리 라이브러리를 사용하는 것이다. 전역으로 관리하는 저장소에서 state를 꺼내 쓸 수 있기 때문에 Props Drilling을 방지할 수 있다.
1. Single source of truth
동일한 데이터는 항상 같은 곳에서 가지고 와야 한다는 의미로 Redux에서는 데이터를 저장하는 단 한개의 공간(Store)이 있음과 연결되는 원칙이다. 사실 여러개의 Store를 사용하는 것은 가능하긴 하나 권장되지는 않는다.
2. State is read-only
상태는 읽기 전용이라는 뜻으로 React에서 상태 갱신 함수(setState)로만 상태를 변경하고 배열은 push가 아닌 concat, 객체의 경우 마찬가지로 기존의 객체는 건드리지 않고 Object.assign를 사용하거나 spread 연산자를 사용하는 것처럼 Redux의 상태도 직접 변경할 수 없음을 의미한다. 즉, Action 객체가 있어야만 상태를 변경할 수 있음과 연결되는 원칙이다.
3. Changes are made with pure functions
변경은 순수 함수로만 가능하다는 뜻으로 상태가 예상치 못한 값으로 변경되는 일이 없도록 순수 함수로 작성되어야하는 Reducer와 연결되는 원칙이다. 동일한 매개변수로 호출된 Reducer 함수는 언제나 똑같은 결과값을 반환해야한다.
Redux에서는 Action → Dispatch → Reducer → Store 순으로 데이터가 단방향으로 흐른다.
npm i redux react-redux
Store는 상태가 관리되는 하나뿐인 저장소이며 state가 저장되는 공간이다.
// 1. store를 손쉽게 사용할 수 있게 하는 컴포넌트인 Provider 불러오기
import { Provider } from 'react-redux';
// 2. redux로 부터 createStore 불러오기
import { createStore } from 'redux';
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
const reducer = () => {};
// 3 변수 store에 createStore 메서드를 통해 reducer 함수 전달
const store = createStore(reducer);
root.render(
// 4. 전역 상태 저장소 store를 사용하기 위해 App 컴포넌트를 Provider로 감싼 후 props를 통해 store 전달
<Provider store={store}>
<App />
</Provider>
);
Reducer는 Dispatch에게서 전달받은 Action 객체의 type 값에 따라서 상태를 변경시키는 함수이다.
// 초기값 = 1
const count = 1
// Reducer 함수의 첫번째 인자에는 default value, 두번째 인자에는 action 객체가 들어간다.
const counterReducer = (state = count, action) => {
// Action 객체의 type 값에 따라 분기하는 switch 조건문
switch (action.type) {
// action === 'INCREASE'일 경우
case 'INCREASE':
return state + 1
// action === 'DECREASE'일 경우
case 'DECREASE':
return state - 1
// action === 'SET_NUMBER'일 경우
case 'SET_NUMBER':
return action.payload
// 해당 되는 Action이 없을 경우 기존 상태를 그대로 반환
// default를 분기해주지 않으면 첫 렌더링 시 undefined가 출력된다.
default:
return state;
}
} // Reducer가 리턴하는 값이 새로운 상태가 된다.
여러 개의 Reducer를 사용하는 경우, Redux의 combineReducers 메서드를 사용해서 하나의 Reducer로 합쳐줄 수 있다.
import { combineReducers } from 'redux';
const rootReducer = combineReducers({
counterReducer,
diffReducer,
...
});
어떤 액션을 취할 것인지 정의해 놓은 객체이며 Dispatch를 통해 Reducer의 두번째 인자로 전달된다.
// payload가 필요 없는 경우
{ type: 'INCREASE' }
// payload가 필요한 경우
{ type: 'SET_NUMBER', payload: 5 }
보통 Action을 직접 작성하기 보다 Action 객체를 생성하는 함수인 액션 생성자(Action Creator)를 만들어 사용하는 경우가 많다.
// payload가 필요 없는 경우
const increase = () => {
return {
type: 'INCREASE'
}
}
// payload가 필요한 경우
const setNumber = (num) => {
return {
type: 'SET_NUMBER',
payload: num
}
}
Dispatch는 Reducer로 Action을 전달해주는 함수이다. Dispatch의 전달인자로 Action 객체가 전달된다.
// Action 객체를 직접 작성하는 경우
dispatch( { type: 'INCREASE' } );
dispatch( { type: 'SET_NUMBER', payload: 3 } );
// 액션 생성자(Action Creator)를 사용하는 경우
dispatch( increase() );
dispatch( setNumber(3) );
앞서 Store, Reducer, Action, Dispatch에 대해 알아보았다. 이제 이 개념들을 연결시켜 주어야 하는데 이때 사용되는 것이 Redux Hooks의 useDispatch()와 useSelector() 메서드이다.
useDispatch() 메서드는 Action 객체를 Reducer로 전달해 주는 Dispatch 함수를 반환하는 메서드이다. 위의 Dispatch 예시에서 useDispatch() 메서드가 사용되었다.
import { useDispatch } from 'react-redux'
const dispatch = useDispatch()
dispatch(increase())
console.log(counter) // 2
dispatch(setNumber(5))
console.log(counter) // 5
// 이벤트 핸들러 내에서 사용
const handlePlus = () => {
dispatch(increase());
};
컴포넌트와 state를 연결하여 Redux의 state에 접근할 수 있게 해주는 메서드이다. state가 필요한 컴포넌트에서 store에 저장된 state를 불러온다.
import { useSelector } from 'react-redux'
const counter = useSelector((state) => state)
console.log(counter) // 1
return (
<div>
{`카운트: ${state}`}
</div>
);