벨로퍼트 리액트 - 6. 리덕스
1) 기본 용어 설명
* 액션(Action)
상태에 어떤 변화가 필요할때 발생되는 객체. { type: "TOGGLE_VALUE" }
type
필드를 필수적으로 가지고 있으며 그 외의 값은 자유롭게 추가할 수 있다.
// 예시
{
type : 'ADD_USER',
data : {
id: 0,
name: 'harry',
age : 11
}
}
{
type: 'CHANGE_INPUT',
text: '변경되는 내용'
}
* 액션 생성함수(Action Creator)
액션을 만드는 함수. 파라미터를 받아와서 액션 객체 형태로 만들어준다.
export
키워드를 붙여 액션 생성함수를 만들어서 사용하면 다른 파일에서 불러와 사용하기 쉽다. 리덕스 사용시 필수적이진 않다.
// 예시
export function addUser(data) {
return {
type: 'ADD_USER',
data,
}
}
// 또는
export function changeInput = text => ({
type: 'CHANGE_INPUT',
text
})
* 리듀서(Reducer)
변화를 일으키는 함수. state(현재 상태)와 action (전달받은 액션) 두가지 파라미터를 받아 새로운 상태를 만들어서 반환한다. useReducer와 동일한 형태. 리덕스 사용시에는 여러개의 리듀서를 만들고 이를 합쳐서 루트 리듀서(Root Reducer)를 만들 수 있다.
function counter(state, action) {
switch (action.type) {
case 'INCREASE' :
return state + 1;
case 'DECREASE' :
return state - 1;
default:
return state;
// useReducer에서는 보통 default부분에 에러를 발생시키도록 처리하는 반면
// 리덕스의 리듀서에서는 기존의 state를 그대로 반환하도록 작성해야한다
}
}
* 스토어(Store)
리덕스에서는 한 애플리케이션 당 하나의 스토어를 만들게 된다. 내부에는 현재의 앱 상태와 리듀서가 들어 있고 추가적으로 몇개의 내장함수가 들어있다.
* 디스패치(dispatch)
스토어의 내장함수. 액션을 발생시킨다. dispatch를 사용할때는 액션을 파라미터로 전달한다. dispatch(action) → 스토어는 리듀서 함수를 실행시켜 해당 액션을 처리하는 로직이 있다면 액션을 참고하여 새로운 상태를 만들어준다.
* 구독(subscribe)
스토어의 내장함수. 함수 형태의 값을 파라미터로 받아온다. subscribe 함수에 특정 함수를 전달해주면 액션이 디스패치 되었을대마다 전달해준 함수가 호출된다.
⇒ 보통 함수를 직접 사용하기보다는 react-redux 라이브러리에서 제공하는 connect 또는 useSelector Hook을 사용하여 구독함
2) 리덕스의 3가지 규칙
(1) 하나의 애플리케이션 안에는 하나의 스토어가 있다
하나의 애플리케이션에서는 단 한개의 스토어를 만들어서 사용한다.
여러개의 스토어 사용도 가능하나 권장되지 않음. 특정 업데이트가 매우 빈번하게 일어나거나 특정 부분을 오나전히 분리시키게 될때 여러개의 스토어를 만들수도 있으나 그렇게 하면 개발 도구를 활용하지 못하게 됨.
(2) 상태는 읽기 전용
기존의 상태는 건드리지 않고 새로운 상태를 생성하여 업데이트 해주는 방식으로 작업해야한다.
⇒ 불변성 유지
(3) 변화를 일으키는 함수, 리듀서는 순수한 함수여야 한다
- 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받는다
- 이전의 상태는 절대로 건드리지 않고, 변화를 일으킨 새로운 상태 객체를 만들어서 반환해야 한다.
- 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과값을 반환해야만 한다.
실행할때마다 다른 결과값이 나타날 수 있는 경우=순수하지 않음
→ new Date() 사용, 랜덤 숫자 생성, 네트워크 요청 등…
⇒ 리듀서 함수의 바깥에서 처리해주어야 함.
3) 리덕스 사용할 준비하기
예제코드
index.js 에 exercise.js 코드 불러오기
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './exercise'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: <https://bit.ly/CRA-vitals>
reportWebVitals();
exercise.js
import { createStore } from 'redux';
// createStore = 스토어를 만들어주는 함수.
// 리액트 프로젝트에서는 단 하나의 스토어를 만든다.
// 리덕스에서 관리할 상태 정의
const initialState = {
counter: 0,
text: '',
list: []
};
// 액션 타입 정의 - 주로 대문자로 작성
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const CHANGE_TEXT = 'CHANGE_TEXT';
const ADD_TO_LIST = 'ADD_TO_LIST';
// 액션 생성함수 정의 - camelCase로 작성
function increase() {
return {
type: INCREASE // 액션 객체의 type 값은 필수
};
};
// 화살표 함수로 사용시 (추천)
const decrease = () => ({
type: DECREASE
});
const changeText = text => ({
type: CHANGE_TEXT,
text // 액션 안에는 type 외에 추가적인 필드를 자유롭게 넣을 수 있다
});
const addToList = item => ({
type: ADD_TO_LIST,
item
});
// 리듀서 만들기
// 위 액션 생성함수들을 통해 만들어진 객체들을 참조하여 새로운 상태를 만드는 함수
// !! 리듀서에서는 불변성을 반드시 지켜야함 !!
function reducer(state = initialState, action) {
// state의 초기값 = initialState
switch (action.type) {
case INCREASE:
return {
...state,
counter: state.counter + 1
};
case DECREASE :
return {
...state,
counter: state.counter - 1
};
case CHANGE_TEXT :
return {
...state,
text: action.text
};
case ADD_TO_LIST :
return {
...state,
list : state.list.concat(action.item)
}
default :
return state;
}
};
// 스토어 만들기
const store = createStore(reducer);
console.log(store.getState()); // 현재 store 안에 들어있는 상태 조회
// 스토어 내부의 상태가 바뀔때마다 호출되는 listener 함수
const listener = () => {
const state = store.getState();
console.log(state);
};
const unsubscribe = store.subscribe(listener);
// 구독을 해제하고싶을때는 unsubscribe() 호출
// 액션 디스패치
store.dispatch(increase());
store.dispatch(decrease());
store.dispatch(changeText('안녕하세요!'));
store.dispatch(addToList({ id: 1, text: '야호'}));
4) 리덕스 모듈 만들기
리덕스 모듈 : 액션타입, 액션 생성함수, 리듀서가 모두 포함된 자바스크립트 파일
각각 다른 파일에 저장할수도 있지만 꼭 분리되어있을 필요는 없다.
리듀서와 액션 관련 코드들을 하나의 파일에 몰아서 작성 ⇒ Ducks 패턴
(1) counter 모듈 만들기
src/modules 디렉터리 만들고 counter.js 파일 생성 후 코드 작성
// 액션 타입 만들기
// Ducks 패턴을 따를때 - 액션의 이름에 접두사 넣기. 타 모둘과 액션이름이 중복되는것을 방지함.
const SET_DIFF = 'counter/SET_DIFF';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
// 액션 생성함수 만들기
// 함수 만들고 export 키워드로 내보내줌!
export const setDiff = diff => ({type:SET_DIFF, diff});
export const increase = () => ({type: INCREASE});
export const decrease = () => ({type: DECREASE});
// 초기 상태 선언
const initialState = {
number: 0,
diff: 1
};
// 리듀서 선언
// 리듀서는 export default 로 내보낸다
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:
return state;
}
};
(2) todos 모듈 만들기
src/modules/todos.js
// 액션 타입 선언
const ADD_TODO = 'todos/ADD_TODO';
const TOGGLE_TODO = 'todos/TOGGLE_TODO';
// 액션 생성함수 선언
let nextId = 1; // todo 데이터에서 사용할 고유 id
export const addTodo = text => ({
type: ADD_TODO,
todo: {
id: nextId++, // 새 항목 추가 후 nextId 값에 1을 더함
text
}
});
export const toggleTodo = id => ({
type: TOGGLE_TODO,
id
});
// 초기상태 선언
// 리듀서의 초기상태는 반드시 객체타입 X . 배열 O 원시타입(숫자, 문자, 불리언) O
const initialState = [
// 객체안에 들어갈 배열
/*
{
id: 1,
text: 'test',
done: false
}
*/
];
export default function todos(state = initialState, action) {
switch (action.type) {
case ADD_TODO :
return state.concat(action.todo);
case TOGGLE_TODO :
return state.map(
todo =>
todo.id === action.id // id가 일치하면
? {...todo, done: !todo.done} // done 값을 반전
: todo // 아니라면 그대로
);
default:
return state;
}
};
(3) 루트 리듀서 만들기
두개의 리덕스 모듈(counter.js, todos.js)의 두개의 리듀서를 하나로 합칠것임.
합쳐진 리듀서 ⇒ 루트 리듀서.
리덕스에 내장된 combineReducers 함수 사용.
modules/index.js
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';
const rootReducer = combineReducers({
counter,
todos
});
export default rootReducer;
스토어 만들기(src/index.js 수정)
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { createStore } from 'redux';
import rootReducer from './modules';
const store = createStore(rootReducer); // 스토어 생성
console.log(store.getState()); // 스토어의 상태 확인
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: <https://bit.ly/CRA-vitals>
reportWebVitals();
(4) 리액트 프로젝트에 리덕스 적용하기
리액트 프로젝트에 리덕스 적용 → react-redux 라이브러리 사용
src/index.js 에서 Provide 컴포넌트 불러와서 App 컴포넌트 감싸기. ⇒ 렌더링하는 어떤 컴포넌트든 리덕스 스토어에 접근할 수 있음
Provider의 props에 store 넣어줌
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './modules';
const store = createStore(rootReducer); // 스토어 생성
console.log(store.getState()); // 스토어의 상태 확인
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: <https://bit.ly/CRA-vitals>
reportWebVitals();