connect
, useSelector
, useDispatch
, useStore
context
를 만드는 수로로움을 없애준다. 애플리케이션의 모든 상태를 redux
로 관리할 필요는 없지만, 다음의 경우에 해당되는 데이터는 redux
로 관리하는 게 좋다.
예를 들어 상품 구매 시 사용자 정보 페이지에서 결제 페이지로 갔다가 뒤로 가기를 클릭한 경우, 이전에 입력했던 사용자 정보를 유지하는 게 좋으므로 redux
로 관리하면 된다. 두 가지 모두 해당되지 않는 데이터는 react
에서 제공하는 컴포넌트의 state
로 관리하면 된다.
Action
상태에서 어떤 변화가 필요할 때 우리는 Action
을 발생한다. 이는 하나의 객체로 표현이 된다.
{
type: "TOGGLE_VALUE",
data: {
id:0,
text: "리덕스 배우기"
}
}
Action
에는 type
이라는 값이 필수적으로 있어야 한다. 나중에 리덕스에서 상태를 업데이트 할 때, type
을 보고 어떻게 업데이트 할지에 대한 정보를 지니고 있는 객체이다.
type
값은 필수로 있어야 하고, 다른 값은 자유롭게 넣어주면 된다 .
{
type: "CHANGE_INPUT",
text: "안녕하세요"
}
이런 action
같은 경우를 보면, CHANGE_INPUT
이라는 액션은 input
의 텍스트 값을 "안녕하세요"라는 값으로 바꾸겠다는 것을 의미한다.
액체 객체를 만들어주는 함수, 단순히 파라미터를 받아와서 액션 객체를 만들어 주는 함수이다.
export function addTodo(data){
return{
type: "ADD_TODO",
data
};
}
//화살표 함수로도 만들수 있다.
export const chageInput = text => ({
type: "CHANGE_INPUT",
text
});
리덕스를 사용 할 때, 액션 생성 함수를 사용하는 것이 필수적이지는 않다. 다만 액션 생성함수를 만들어 놓으면 더 편하게 액션 객체를 만들 수 있다. 만약 액션생성함수를 사용하지 않으면 발생시킬 때마다 액션 객체를 작성해 주면 된다. 꼭 쓸필요는 없지만 사용하면 편하다.
reducer
는 store
가 가지고 있는 state
를 변화 시켜주는 함수이다. 즉, 상태를 바꿔주거나 새로운 상태를 만들어 준다. 두 가지 파라미터(state, action)를 가져오고 action의 type에 따라 state를 변화 시킨다.
function counter(state, action) {
switch (action.type) {
case "INCREASE":
return state +1;
case "DECREASE":
return state -1;
default:
return state;
}
}
action type
을 가지고 action type
이 무엇이냐에 따라서 다른 업데이트 작업을 한다. 리듀서에서는 불변성을 꼭 유지해주어야 한다. 그리고 default
에서 기존의 state
를 그대로 반환하는 형태로 작성해 주어야 한다. 리덕스를 사용할때는 여러개의 reducer를 만들고 이를 합쳐서 루트 리듀서를 만들수 있다. 그리고 루트 리듀서 안에 있는 작은 리듀서들을 서브 리듀서라고 한다.
리덕스를 사용하게 되면 한 어플리케이션 당 하나의 스토어가 필요하다. 스토어 안에는 현재 앱의 상태와 리듀서가 들어있고, 추가적으로 몇 가지 내장 함수가 들어있다.
액션을 발생시키고, 액션을 스토어에 전달한다.
dispatch ({ type: "INCREASE" })
디스패치라는 함수는 다음과 같이 사용하고, 액션 객체를 만들어서 디스패치 파라미터로 넣어서 호출을 한다. 그렇게 호출 하게 되면 해당 액션이 리듀서로 전달하게 되서, 리듀서 함수에서 새로운 상태를 반환하게 되면 스토어의 상태가 새로워 진다.
스토어의 내장함수중 하나이다. subscribe
을 호출할 때 파라미터로 특정 함수로 넣어주면 액션이 디스패치 될때마다 우리가 설정한 함수가 호출이 된다. 스토어의 상태가 업데이트가 될때마다 특정 함수를 호출할 수가 있다. 리액트에서 리덕스를 사용하게 될때 이 함수를 직접 사용하지 않는다. 대신에 리액트 리덕스 라이브러리에서 제공하는 connect 함수
useSelector
hook을 사용해서 스토어 안에있는 상태가 업데이트 되면 컴포넌트가 리 렌더링되는 작업을 대신 처리해준다.
1. store는 오직 하나만 존재한다.
application
에는 하나의 store
가 있다. 즉 store
를 한개 이상을 만들지 말것을 권장한다. 디버깅이 쉬워지는 장점도 있다. 2. state는 읽기 전용이다.
state
는 읽기 전용이다. 불변성을 지켜주어야 한다. 리덕스에서 state
의 불변성을 유지하는 것은 좋은 성능을 지켜내기 위함이다. 불변성을 지켜야만 컴포넌트들이 제대로 리렌더링 된다. state
를 변경하고 싶다면, Action
을 dispatch
해서 state
를 변경해야 한다. 3. reducer는 순수 함수여야 한다.
reducer
함수는 이전 상태와, 액션 객체를 파라미터로 받는다. 이전의 상태는 절대 변경하지 않고, 변화를 일으킨 새로운 state
객체를 만들어 return
한다. 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과값을 반환해야 한다. 즉 동일한 인풋이 있을때는 동일한아웃풋이 있어야 한다.액션타입, 액션생성함수, 리듀서가 모두 들어있는 자바스크립트 파일을 의미한다. 리덕스를 사용하기 위해 위 항목들은 각각 다른 파일들로 저장 할 수 있다.
modules/counter.js
//Action 타입 선언
// Ducks 패턴을 사용할때는 액션 타입을 선언할때 문자열 앞에 접두사는 붙이는데, 다른 모듈과 이름이 중복되지 않게 하기 위함이다.
const SET_DIFF = 'counter/SET_DIFF'; //몇씩 더할지 정해준다.
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
//Action creator함수 선언
export const setDiff = diff => ({ type : SET_DIFF, diff});
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
//reducer에서 관리할 초기상태 선언 (module의 초기상태)
const initialState = {
number : 0,
diff : 1
};
//reducer정의
//state에는 기본값(initialState)을 설정해 주어야 한다.
export default function counter(state = initialState, action){
switch (action.type){
case SET_DIFF:
return{
...state,
diff: action.diff
};
case INCREASE:
return{
...state,
number: state.number + state.diff
};
case DECREASE:
return {
...state,
number: state.number - state.diff
};
default: //자신이 처리할수 없는 action의 경우 기존 state를 return해준다.
return state;
}
}
modules/index.js
import {combineReducers} from "redux";
import counter from "./counter";
import todos from "./todos";
const rootReducer = combineReducers({
counter,
todos
});
export default rootReducer;
redux
두개를 합쳐서 index.js
파일에 rootReducer
를 만든다.
다음은 리액트 프로젝트에 리덕스를 적용해 볼것이다. 리액트 프로젝트에 리덕스를 적용하기 위해서는 패키지 하나를 설치해 주어야 한다.
$ yarn add react-redux
src 디렉토리에 components라는 디렉토리를 만든다. components안에 presentational 컴포넌트를 만들건데 이것은 리덕스 스토어에 직접 접근하지 않고, 필요한 값 또는 함수를 props로만 받아서 사용하는 컴포넌트 이다.
conponents/Counter.js
import React from 'react';
const Counter = (number, diff, onIncrease, onDecrease, onSetDiff) => {
const onChange = e => {
onSetDiff(parseInt(e.target.value, 10)) //input의 값은 문자열이다. 이것을 숫자로 변환해 주기 위해 parseInt를 사용한다.
}
return (
<div>
<h1>{number}</h1>
<div>
<input type="number" value= {diff} onChange={onChange} />
<button onClick={onIncrease}>+</button>
<button onClick={onDecrease }>-</button>
</div>
</div>
);
};
export default Counter;
Counter.js
라는 presentational
컴포넌트에서는 UI를 선언하는것에 집중했다. 필요한 값이나 함수는 props
로 받아와서 사용한다.
number
, diff
, onIncrease
, onDecrease
, onSetDiff
들을 props
로 받아온걸 알수 있다.
container/CounterContainer.js
import React from 'react';
import Counter from "../components/Counter";
import {useSeletor, useDispatch} from "react-redux"; //상태를 조회하기 위해서 사용
import { increase, decrease, setDiff } from '../modules/counter';
const CounterContainer = () => {
const { number, diff } = useSeletor(state => ({
number: state.counter.number,
diff: state.counter.diff
}));
const dispatch = useDispatch();
// dispatch({ type: 'counter/INC'}) 이처럼 dispatch를 사용해서 특정 액션을 발생시킬수 있다.
//하지만 우리가 액션 생성함수가 있기 때문에 굳이 이것을 사용할 필요가 없다.
//여기서 만든 액션 생성 함수들이 호출되면 액션객체가 만들어 져서 dispatch가 된다.
const onIncrease = () => dispatch(increase());
const onDecrease = () => dispatch(decrease());
const onSetDiff = diff => dispatch(setDiff(diff));
return (
<Counter number = {number}
diff = {diff}
onIncrease = {onIncrease}
onDecrease = {onDecrease}
onSetDiff = {onSetDiff}
/>
//리액트 컴포넌트에서 리덕스를 연동할때는 이와같이 useSelector, useDispatch라는 훅스를 사용한다.
//useSelector 는 상태를 조회하는 훅이다.
);
};
export default CounterContainer;
containers
컴포넌트란, 리덕스에 있는 상태를 조회하거나 액션을 dispatch
할수있는 컴포넌트를 의미한다.
상태관리는 container컴포넌트에서 해준것을 알수 있다.
CounterContainer.js
에서 리덕스 스토어를 불러오고, 어떤 함수가 호출되면 액션 dispatch
하는 작업을 수행한다.
하지만 이런식으로 컴포넌트를 UI만 선언하고, 리덕스 상태관리 해주는 식으로 구분지어서 만들 필요는 없다. 그냥 본인이 편한대로 선택해서 만들자.
redux
개발자 도구를 사용하면 현재 스토어의 상태를 개발자 도구에서 조회가능하고, 지금까지 어떤 action
들이 dispatch
되었는지, 그리고 액션에 따라 상태가 어떻게 변화되어 왔는지 등등을 확인할수 있다.
리덕스 개발자 도구를 적용하기 위해서는 프로젝트에서 패키지를 설치해야 한다.
$ yarn add redux-devtools-extension
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from "react-redux";
import { createStore } from 'redux';
import rootReducer from "./modules";
import { composeWithDevTools } from "redux-devtools-extension"; // composeWithDevTools 에서 불러온다.
const store = createStore(rootReducer, composeWithDevTools()); // 해당 함수는 붙이면 끝.
console.log(store.getState());
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);