1. 상태 관리 라이브러리가 왜 필요할까?
공통된 부모 컴포넌트는 조상인 최상위 컴포넌트 뿐인 공통된 부모 컴포넌트가 없는 2개의 컴포넌트에서 각각 사용되는 상태가 있다고 가정할 때, 기존 리액트 데이터 흐름에 따르면 최상위 컴포넌트에 상태를 위치시키는게 적합하다.
하지만 이런 경우, 해당 상태를 직접 사용하지 않는 최상위 컴포넌트, 부모 컴포넌트들도 상태 데이터를 가지며 해당 컴포넌트가 사용하지도 않는 상태인데도 상태를 써야 하는 자식 컴포넌트까지 props를 내려주거나 상태를 끌어올리는 이벤트 드릴링이 일어나야 하므로 데이터 흐름이 복잡해지고 컴포넌트 구조의 변경이 생기는 경우 데이터 흐름을 완전히 바꿔야 할 수도 있는 등 유연하게 대처할 수 없어 매우 비효율적이다.
따라서 리액트의 상태 관리 라이브러리인 React-Redux를 사용하여 전역 상태를 관리하는 저장소인 Store에서 데이터가 필요한 컴포넌트에게만 상태를 전달해주거나 상태를 전달받는다.
2. Redux에서 사용하는 Action, Dispatcher, Reducer 그리고 Store의 의미와 특징
- 단방향 데이터 흐름: Action 객체 → Dispatch 함수 → Reducer 함수 → Store
- 상태 변경 이벤트 발생 시 변경될 상태에 대한 정보가 담긴 Action 객체가 생성된다.
- 이 Action 객체는 Dispatch 함수의 인자로 전달된다.
- Dispatch 함수는 Action 객체를 Reducer 함수로 전달한다.
- Reducer 함수는 Action 객체의 값을 확인하고 그 값으로 전역 상태 저장소 Store의 상태를 변경한다.
- 상태가 변경되고 화면을 재렌더링.
2.1 Action 객체
어떤 액션을 취할 지 정의해 놓은 객체이다. payload 필요 유무에 따라 구성이 달라진다.
type 프로퍼티는 해당 Action 객체가 어떤 동작을 하는지를 명시하는 역할을 하기 때문에 필수로 지정을 해 주어야 한다.
보통 Action 객체를 직접 작성하기 보다는 액션 생성자(Action Creater) 함수를 만들어 사용한다.
// payload가 불필요한 경우
const increase = () => {
return { type: 'INCREASE' };
};
// payload가 필요한 경우
const setNumber = num => {
return {
type: 'SET_NUMBER',
payload: num
};
};
2.2 Dispatch 함수(dispatcher)
단방향 데이터 흐름: Action 객체 → Dispatch 함수 → Reducer 함수 → Store
Dispatch 함수는 Action 객체를 인수에 넣어 Reducer 함수로 전달해주는 함수이다.
Dispatch 함수는 Action 객체를 전달받으면 Reducer 함수를 호출한다.
// useDispatch : Action 객체를 Reducer로 전달해 주는 메서드.
import { useDispatch } from 'react-redux';
const dispatch = useDispatch();
// 1. Action 객체를 직접 작성한 경우
dispatch( { type: 'INCREASE' } );
dispatch( { type: 'SET_NUMBER', payload: 5 } );
// 2. 액션 생성자 함수 사용 시
dispatch( increase() );
dispatch( setNumber(5) );
2.3 Reducer
Reducer 함수는 Dispatch에게서 전달받은 Action 객체의 type 값에 따라 상태를 변경시키는 함수이다.
이 때, Reducer는 순수함수 이어야 함에 유의한다. 외부 상태에 따라 값이 변경되어선 안되기 때문이다.
Reducer 함수 정의 시 현재 상태와 어떻게 바꿀 것인지의 2가지 포인터를 반드시 매개변수로 넣어야 한다.
const count = 1;
// Reducer 함수 정의 시 현재 상태와 어떻게 바꿀 것인지의 2가지 포인터를 인자로 요구함.
cosnt counterReducer = (state = count, action) {
// Switch문 사용: Action 객체의 type 값에 따라 분기하기 위해.
// if문 써도 상관은 없음.
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;
// 해당되는 경우가 없을 때 리턴하는 디폴트값
default:
return state;
}
}
2.3.1 Reducer 함수 안에서 state 초기값 지정 방법 2가지
- state가 값이 없는 경우(undefined) 조건 분기
- 매개변수 선언시에 초기화
// 1. state가 값이 없는 경우(undefined) 조건 분기
export const reducer = (initialState, action) => {
if (typeof initialState === undefined) {
initialState = 1;
}
// ... 생략
};
// 2. 매개변수 선언시에 초기화
export const reducer = (**initialState = 1**, action) => {
// ... 생략
};
여러 개의 Reducer를 사용하려면 Redux의 combineReducers 메서드를 사용해서 하나의 Reducer로 합쳐줄 수 있다.
2.3.2 Action 객체의 payload가 있는 경우 Reducer 함수 리턴값
// reducer.js
export const reducer = (initialState = "", action) => {
switch (action.type) {
**case '더하기':
return action.payload;**
default:
return initialState;
}
};
// App.js
export default function App() {
const text = useSelector((state) => state);
const dispatch = useDispatch();
const plusNum = () => {
dispatch({ **type: '더하기',** **payload: '더해주세요!'** });
};
console.log(text); //'더해주세요!'
return (
<Container>
<Text>{text}</Text>
<Button onClick={plusNum}>👍</Button>
</Container>
);
}
2.4 Store
Store는 전역의 상태를 관리하는 오직 하나뿐인 저장소이다.
createStore 메서드를 활용해 Reducer를 연결하여 Store를 생성한다.
import { createStore } from 'redux';
const store = createStore(rootReducer);
2.5 Redux Hooks
Redux Hooks는 리액트에서 Redux를 사용할 때 활용할 수 있는 Hooks 메서드를 제공한다.
- useSelector : 컴포넌트와 state를 연결하여 Redux의 state에 접근할 수 있게 해주는 메서드.
- useDispatch : Action 객체를 Reducer로 전달해 주는 메서드.
- Redux Hooks 메서드는 'redux'가 아니라 'react-redux' 에서 불러온다.
// 1. provider : subscribe 할(상태관리할) 컴포넌트를 감싸는 컴포넌트.
// 2. useSelector : 어떤 state 값을 쓸 지 선택.
// 3. useDispatch : state 값을 변경 시 사용.
import { Provider, useSelector, useDispatch } from 'react-redux';
// ... 생략
const number = useSelector(state => state.number);
console.log(counter); // 1
2.6 Redux의 3가지 원칙이 주요 개념과 어떻게 연결될까?
- Single source of truth → 동일한 데이터는 항상 같은 곳에서 가지고 와야 한다. redux에서 저장소는 store 하나 뿐이다.
- State is read-only → 상태는 state 변경함수로만 변경할 수 있던 것과 같이 redux의 상태도 Action 객체가 있어야만 변경할 수 있다.
- Changes are made with pure functions → 변경은 순수함수로만 가능하다. 의도하지 않게 외부 상태가 값을 변경시켜선 안되기 때문에 Reducer를 순수함수로 작성한다.
3. 구현
createStore 메서드를 import하고 provider 컴포넌트의 속성으로 store를 사용하여 전역 상태를 관리할 저장소 store 생성.
Provider 컴포넌트로 상태를 관리할 컴포넌트들을 상위에서 감싼다.
dispatch 함수를 실행한 컴포넌트에서 dispatch 함수가 reducer 함수를 실행시키고 reducer 함수의 2번째 매개변수인 action에서 조건 분기로 Action 객체의 type에 따라 분기한 대로 state를 변경.
reducer 함수는 순수함수이어야 한다. 따라서 reducer 함수의 첫번째 매개변수인 currentState(현재의 상태값)의 값을 변경시키지 않고 새로 변수를 만들어 currentState를 deep copy 하여 이 복사본으로 값의 변경을 적용시켜 값의 불변성을 유지하였다.
const newState = { ...currentState };
3.1 Redux 사용 상태관리 예제 개요
- root 컴포넌트(App)의 state인 number 상태값이 다른 컴포넌트를 거치지 않고 바로 Left3 컴포넌트로 전달된다.
- Right3 컴포넌트의 버튼 클릭 시 다른 컴포넌트를 거치지 않고 바로 Left3 컴포넌트의 상태값을 변경한다.
3.2 Redux Hooks
- provider : 상태가 변경될 컴포넌트를 감싸는 컴포넌트.
- useSelector : 어떤 state 값을 쓸 지 선택.
- useDispatch : state 값을 변경 시 사용.
import React, { useState } from 'react';
import './style.css';
import { createStore } from 'redux'; // Store 생성 메서드 import
import { Provider, useSelector, useDispatch } from 'react-redux';
// 1. provider : 상태가 변경될 컴포넌트를 감싸는 컴포넌트.
// 2. useSelector : 어떤 state 값을 쓸 지 선택.
// 3. useDispatch : state 값을 변경 시 사용.
// * Reducer 함수 정의
function reducer(currentState, action) {
// * 현재 상태가 값이 없는 경우 기본 상태값을 리턴.
if (currentState === undefined) {
return { number: 1 };
}
// 과거 state의 deepCopy한 복제본 newState로 값의 변경을 다루어 값의 불변성 유지.
const newState = { ...currentState };
// dispatch 함수가 reducer 함수를 호출하므로 아래 if문 정의.
if (action.type === 'INCREASE') {
newState.number++;
}
return newState; // 반환값이 새로운 State가 되어 Store에 반영됨.
}
const store = createStore(reducer);
export default function App() {
const [number, setNumber] = useState(1);
return (
<div id="container">
<h1>Root : {number}</h1>
<div id="grid">
{/* Provider에 store 속성 필수 */}
<Provider store={store}>
<Left1></Left1>
<Right1></Right1>
</Provider>
</div>
</div>
);
}
function Left1(props) {
return (
<div>
<h1>Left1 </h1>
<Left2></Left2>
</div>
);
}
function Left2(props) {
// 상태 사용한 컴포넌트만 재렌더링되는지 확인용 코드
console.log('Left2 component re-rendered');
return (
<div>
<h1>Left2 : </h1>
<Left3></Left3>
</div>
);
}
function Left3(props) {
// 상태 사용한 컴포넌트만 재렌더링되는지 확인용 코드
console.log('Left3 component re-rendered');
// * useSelector 사용해서 state 직통으로 받아오기
// * useSelector의 인수로 콜백함수를 정의하거나 외부에서 정의한 함수명을 넣어주어야 한다.
const number = useSelector(state => state.number);
return (
<div>
<h1>Left3 : {number}</h1>
</div>
);
}
function Right1(props) {
return (
<div>
<h1>Right1</h1>
<Right2></Right2>
</div>
);
}
function Right2(props) {
return (
<div>
<h1>Right2</h1>
<Right3></Right3>
</div>
);
}
function Right3(props) {
// * Right3 컴포넌트의 버튼이 Left3의 number 상태를 변경할 수 있도록 dispatch 함수 사용
const dispatch = useDispatch(); // dispatch 함수 사용방법
return (
<div>
<h1>Right3</h1>
<input
type="button"
value="+"
onClick={() => {
// dispatch 함수가 실행되며 reducer 함수를 호출.
dispatch({ type: 'INCREASE' });
}}
></input>
</div>
);
}
참고자료
- 코드스테이츠 유어클래스 컨텐츠
- 생활코딩 react-redux (2022년 개정판) https://www.youtube.com/watch?v=yjuwpf7VH74
'개발 > React' 카테고리의 다른 글
useMemo (0) | 2022.07.27 |
---|---|
[React] JSX문법 (0) | 2022.03.23 |