💁♀️ 리덕스(Redux)란,
상태 관리를 하기 위한 라이브러리
앱의 상태 전부는 하나의 저장소(store) 안에 있는 객체 트리에 저장 됨.
상태 트리를 변경하는 유일한 방법은 어떤 행동이 일어날지에 해당하는 action이며, 이 action에 따라 상태를 어떻게 변경할지를 명시하기 위한 함수를 작성하고 이를 reducer함수라고 함
🙋 사용 시, 장점
- 애플리케이션의 구조가 단순해짐
- 상태 관리 로직을 테스트하기 쉬워지며, 디버깅 용이
🤔❓ REDUX 실행 순서
useEffect내에서 dispatch(func)
👉 middleware(redux-thunk) : func 호출
👉 dispatch(aciton)
👉 middleware(redux-thunk) : action 객체 넘기기
👉 middleware(redux-logger)
👉 reducer 호출
👉 반환 된 state가 store에 저장
createActions
/* action */ const INCREMENT = 'count/INCREASE'; const DECREMENT = 'count/DECREASE'; /* createActions 사용 여러 개의 액션 함수를 한 번에 생성 가능. 접두사가 있기 대문에 대괄호를 이용해서 키 식별자를 작성. 전달하는 객체의 key 값이 action의 type 속성 값이 되고, value 값의 함수가 반환하는 값이 payload의 속성 값이 됨 action type 속성 값에 따라 반환 되는 객체의 키 값이 설정 됨 EX) actions.count.increment */ const { count : { increase, decrease } } = createActions({ // 구조분해할당으로 count 안에 있는 increment, decrement를 꺼내기 [INCREMENT] : (amount = 1) => ({ incrementValue : amount }), // key : value 형태로 선언 [DECREMENT] : (amount = 1) => ({ decrementValue : amount }) });
handleActions
/* reducer */ /* reducer 함수도 handleActions 기능을 사용하여 간결하게 작성 가능 첫 번째 인자로 state를 전달 받고 두 번째 인자로 action을 전달 받음 중첩 구조분해할당({ payload : { incrementValue })으로 payload 값을 꺼내 콜백 함수 안에서 사용 가능 dispatch 호출 시 전달 된 action의 type과 일치하는 함수를 동작시키게 됨 */ const reducer = handleActions({ // type(key) : 콜백함수(value) [INCREMENT] : (state, { payload : { incrementValue } }) => { return state + incrementValue; }, [DECREMENT] : (state, { payload : { decrementValue } }) => { return state + decrementValue; } }, initialState); // handleActions의 두 번째 인자로 다루는 state의 초기값 설정
useDispatch
useSelector
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { callGetMenuListAPI } from "../../API/MenuAPICall";
import MenuItem from "../items/MenuItem";
function MenuList() {
const dispatch = useDispatch();
const result = useSelector(state => state.menuReducer); // 변화된 state안의 값을 꺼내옴
const menuList = result.menulist;
console.log('result : ', result);
useEffect(
() => {
/* menuList 호출 API */
dispatch(callGetMenuListAPI()); // MenuAPICall에 작성한 함수 호출하여 retuen되는 함수 자체를 받아옴
},[]
)
return (
<div className="menuBox">
{ menuList && menuList.map(menu => <MenuItem key={ menu.id } menu={ menu }/>) }
</div>
);
}
export default MenuList;
import { useDispatch, useSelector } from "react-redux";
import { callGetMenuAPI } from "../../API/MenuAPICall";
import { useEffect } from "react";
function Menu({ id }) {
const dispatch = useDispatch();
const result = useSelector(state => state.menuReducer); /* 꺼내고 싶은 state를 useSelector를 이용하여 꺼내기 */
const menu = result.menu;
console.log('result : ', result);
useEffect(
() => {
/* Menu 호출 API */
dispatch(callGetMenuAPI(id));
},[]
);
return (
<>
{ menu &&
<>
<h3>메뉴 이름 : { menu.menuName }</h3>
<h3>메뉴 가격 : { menu.menuPrice }</h3>
<h3>메뉴 종류 : { menu.categoryName }</h3>
<h3>메뉴 상세 : { menu.detail.desciption }</h3>
<img src={ menu.detail.image } style={{ maxWidth : 500 }} alt={ menu.menuName } />
</>
}
</>
);
}
export default Menu;
💁♀️ 코드 분석
useDispatch()
는 컴포넌트 내부에서 action creator 함수를 dispatch할 때 사용
- dispatch 함수는 해당 action을 store로 보내서 reducer 함수를 실행useSelector()
는 store의 state에 접근할 수 있게 해주는 함수
- 이 함수는 전체 store의 state를 인자로 받아서 필요한 값을 추출하여 반환
- 이를 통해 컴포넌트가 필요한 데이터만을 추출할 수 있게 함
useSelector()
를 사용해서 menuReducer에서 필요한 menulist를 추출하여 menuList 변수에 할당하고 이를 활용하여 menuList 데이터를 화면에 렌더링
미리 axios 라이브러리를 이용하여 서버로 HTTP 요청을 보내는 함수를 정의
axios
async
await
import axios from 'axios';
const DOMAIN = 'http://localhost:4000';
export const request = async (method, url, data) => {
return await axios({
method,
url : `${DOMAIN}${url}`,
data
})
.then(res => res.data)
.catch(error => console.log(error));
}
dispatch()로 action객체 전달하며 reducer함수 호출
import { getMenu, getMenulist } from "../modules/MenuModule";
import { request } from "./Api"
/* 서버로부터 메뉴 목록을 가져오는 API를 호출 */
export function callGetMenuListAPI() {
console.log('getMenuList api calls...📞')
/* 반환되는 비동기 처리 함수가 dispatch의 매개변수로 전달되게 되면 redux-thunk라는 미들웨어에서 아래 함수가 호출되게 됨 */
return async (dispatch, getState) => {
/* axios 라이브러리를 이용한 데이터 요청 */
// Api.js 파일에 있는 request함수에 보낼 때 매개변수 => (요청 방식, 도메인 뒤에 붙을 url, 같이 보낼 data가 있다면 data)
const result = await request('GET', '/menu')
console.log('getMenuList result : ', result);
/* dispatch의 매개변수로 action 객체를 전달하여 store에 state를 저장하게 함 (리듀서 함수 호출) */
dispatch(getMenulist(result));
}
}
/* 서버로부터 id와 일치하는 메뉴를 가져오는 API를 호출 */
export function callGetMenuAPI(id) {
console.log('getMenu api calls...📞')
return async (dispatch, getState) => {
const result = await request('GET', `/menu/${ id }`);
console.log('getMenu result : ', result);
dispatch(getMenu(result));
}
}
💁♀️ 코드 분석
위 Api 코드는 axios 라이브러리를 이용하여 서버로 HTTP 요청을 보내는 함수 request를 정의하는 코드.
request 함수
는 매개변수로HTTP 요청 방식(method)
,요청할 URL(url)
, 그리고요청 데이터(data)
를 받음. axios 라이브러리의 axios() 메소드를 이용하여 서버에 HTTP 요청을 보내고, 이 메소드는 Promise 객체를 반환.- Promise 객체는 비동기 처리를 위한 객체로,
.then()
메소드와.catch()
메소드를 이용하여 비동기 작업이 완료될 때까지 기다렸다가 결과 값을 받아 처리할 수 있음.- 위 코드에서는
.then()
메소드를 이용하여 서버로부터 받은응답 데이터(res.data)
를 반환하고,.catch()
메소드를 이용하여 오류 발생 시 콘솔에 에러를 출력.- 👉
request 함수
는 매개변수로 전달된 method, url, data를 이용하여 axios 라이브러리를 이용해 서버에 HTTP 요청을 보내고, 응답 데이터를 반환하는 함수.
위 MenuAPICall 코드는 서버로부터 메뉴 목록을 가져오기 위해 API를 호출하는 함수인 callGetMenuListAPI()를 정의하는 코드.
callGetMenuListAPI()
함수는 async 키워드를 사용하여 비동기 처리 함수를 반환하며, 반환되는 함수는 redux-thunk 미들웨어에서 호출. 이 함수의 매개변수로는dispatch
와getState
가 전달.- 함수 내부에서는 axios 라이브러리를 이용하여 서버로부터 메뉴 목록 데이터를 요청하며,
request() 함수
를 이용하여 요청을 보내게 됨. 이 때 요청 방식은 GET이며, 요청 URL은 /menu.- 데이터 요청이 완료되면, 반환되는 결과값 result를 이용하여
getMenulist()
액션 객체를 생성하고,dispatch()
함수를 이용하여 해당 액션 객체를 전달하여 store에 state를 저장.- 이렇게 저장된 state는
리듀서 함수
에서 관리.
import { createActions, handleActions } from 'redux-actions';
/* 초기값 */
const initialState = {};
/* 액션 타입 */
const GET_MENULIST = 'menu/GET_MENULIST'; // 메뉴 리스트 조회
const GET_MENU = 'menu/GET_MENU'; // 메뉴 조회
/* 액션 함수 */
export const { menu : { getMenulist, getMenu } } = createActions({
[GET_MENULIST] : (res) => ({ menulist : res }), // 응답값(value)을 menulist 라는 key에 담음
[GET_MENU] : (res) => ({ menu : res })
});
/* 리듀서 함수 */
const menuReducer = handleActions({
// 액션이 전달되어 오면 타입을 기준으로 함수가 호출되고, 그 반환 값이 store에 저장하고자 하는 state임
[GET_MENULIST] : (state, { payload }) => {
return payload;
},
[GET_MENU] : (state, { payload }) => {
return payload;
}
}, initialState);
/* menuReducer를 export하여 index.js에서 import하여 사용 */
export default menuReducer;
💁♀️ 코드 분석
초기값 (initialState)
: Redux Store에서 사용할 초기값을 정의. 이 값은 state가 undefined일 때 사용.액션 타입 (action types)
: Redux Store에서 사용할 액션의 종류를 정의. 액션 타입은 문자열 상수로 정의되며, 액션 객체를 생성할 때 type 프로퍼티로 사용.액션 함수 (action creators)
: 액션 객체를 생성하는 함수. 이 함수를 호출하면 액션 객체가 생성되며, 이때 type 프로퍼티는 액션 타입 상수를 사용.리듀서 함수 (reducer function)
: 액션을 받아서 이전 상태를 새로운 상태로 변경하는 함수이며, Redux Store의 상태를 변경하기 위해 사용. 액션 타입에 따라 상태를 변경하는 로직이 구현되며, state와 action을 매개변수로 받아서 새로운 state를 반환.
위 코드에서는
createActions
함수를 사용하여 액션 함수를 생성하고,handleActions
함수를 사용하여 리듀서 함수를 작성.createActions
함수는 리덕스 액션 객체를 생성하는 함수를 자동으로 생성해주며, 액션 타입과 액션 생성 함수를 정의할 때 코드 양을 줄일 수 있음. handleActions 함수는 액션 타입과 리듀서 함수를 매핑하여 하나의 리듀서 함수를 생성.
menuReducer
: 최종적으로 만들어진 리듀서 함수. 액션 타입에 따라 state를 변경하는 로직이 구현되어 있음. 이 함수를 Redux Store의 reducer로 등록하면, 액션에 따라 state가 변경.
위 코드에서
{ menu : { getMenulist, getMenu } }
는 반환된 객체에서 menu 속성의 값을 가져오는데, 이 값은 GET_MENULIST와 GET_MENU라는 액션 타입으로 생성된 액션 생성 함수를 갖고 있는 객체.
즉, menu 객체는 GET_MENULIST와 GET_MENU 액션 타입에 해당하는 액션 생성 함수를 갖고 있으며, 이 함수들을 getMenulist와 getMenu에 할당. 이렇게 함으로써, 나중에 getMenulist나 getMenu 함수를 호출할 때, menu/GET_MENULIST 또는 menu/GET_MENU와 같이 액션 타입을 지정하여 사용할 수 있음.
import { combineReducers } from 'redux';
import menuReducer from './MenuModule';
import userReducer from './UserModule';
const rootReducer = combineReducers({
menuReducer, userReducer // 성격이 다른 리듀서 함수를 하나로 묶음
});
/* rootReducer를 export하여 Store.js에서 import하여 사용 */
export default rootReducer;
import { applyMiddleware, legacy_createStore as createStore } from "redux";
import rootReducer from "./modules/Index";
import { composeWithDevTools } from 'redux-devtools-extension';
import ReduxThunk from 'redux-thunk';
import ReduxLogger from 'redux-logger';
/* createStore deprecated 되어 있으나 구동에 문제는 없으며 이름을 명시적으로 legacy_createStore as createStore로 작성하면 사라짐
deprecated 시킨 이후는 향후 @reduxjs/tookit를 이용하여 사용할 것을 명확하게 권고하기 위해서임 */
const store = createStore(
rootReducer, // 리듀서 함수 설정
composeWithDevTools(applyMiddleware(ReduxThunk, ReduxLogger)) // 사용할 미들웨어들 설정
);
export default store;
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import store from './Store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={ store }>
<App />
</Provider>
);