리덕스를 사용할 때는 보통 action은 action끼리, reducer는 reducer끼리 등등 모양새대로 분리해서 작성한다. 덕스 구조는 모양새 대신 기능으로 묶어서 작성하는 구조다.
ex) 버킷리스트용 action, actionCreator, reducer를 모두 한 파일에 넣음
// widgets.js
// Actions
const LOAD = 'my-app/widgets/LOAD';
const CREATE = 'my-app/widgets/CREATE';
const UPDATE = 'my-app/widgets/UPDATE';
const REMOVE = 'my-app/widgets/REMOVE';
// Reducer
export default function reducer(state = {}, action = {}) {
switch (action.type) {
// do reducer stuff
default: return state;
}
}
// Action Creators
export function loadWidgets() {
return { type: LOAD };
}
export function createWidget(widget) {
return { type: CREATE, widget };
}
export function updateWidget(widget) {
return { type: UPDATE, widget };
}
export function removeWidget(widget) {
return { type: REMOVE, widget };
}
// side effects, only as applicable
// e.g. thunks, epics, etc
export function getWidget () {
return dispatch => get('/widget').then(widget => dispatch(updateWidget(widget)))
}
action
액션 타입을 정해주는 부분
my-app
프로젝트이름widgets
모둘명, 리듀서명LOAD
,CREATE
,UPDATE
,REMOVE
액션
reducer
export default function reducer(state = {}, action = {})
파라미터 = {}
기본값을 주는 행위로 파라미터에 값이 없으면 빈 덕셔너리라는 의미.
actionCreator
export function createWidget(widget) { return { type: CREATE, widget }; }
자바스크립트에서는 딕셔너리의 key, value가 일치하면 생략가능하기 때문에 딕셔너리 형태가 아니라 변수명 하나만 있는 것
ex) {widget: widget} = { widget }
return
의widget
은export function createWidget(widget)
에서 그대로 받아오는 것이다.
src/redux/modules/bucket.js
상위 폴더들을 만들고 modules 안에 bucket.js 파일을 생성한다.
bucket.js
// Actions const CREATE = 'bucket/CREATE'; const initialState = { list: ["영화관 가기", "매일 책읽기", "수영 배우기", "리액트 강의 수강"] }; // Action Creators export function createBucket(bucket) { console.log("액션 크리에이터: 액션생성"); return {type: CREATE, bucket}; } export function deleteBucket(bucket_index){ console.log("삭제할 버킷 인덱스", bucket_index); return {type: DELETE, bucket_index}; } // Reducer export default function reducer(state = initialState, action = {}) { switch (action.type) { case "bucket/CREATE": { console.log("리듀서:값을 바꿔줌"); const new_bucket_list = [...state.list, action.bucket]; return {list : new_bucket_list}; } }
리덕스 모듈 예제를 버킷리스트에 맞게 수정
// side effects, only as applicable
// e.g. thunks, epics, etc
export function getWidget() {
return dispatch =>
get('/widget').then(widget => dispatch(updateWidget(widget)))
}
미들웨어(데이터를 외부에서 가져와야 하는 경우 데이터를 즉시 리듀서로 넘겨줄 수가 없기 때문에 대신 중간다리를 놓아주는 역할)는 버킷리스트에서 필요가 없기에 삭제했다.
const CREATE = 'bucket/CREATE';
const initialState = {
list: ["영화관 가기", "매일 책읽기", "수영 배우기", "리액트 강의 수강"]
};
액션 타입을 정해주는 부분이다. CREATE 외에는 필요 없는 기능이므로 삭제한다.
덕스구조에는 없는 내용이지만 App.js
에 있던 list
를 가져와서 initialState
라는 이름으로 초기값을 생성한다.
export function createBucket(bucket) {
return {type: CREATE, bucket};
}
액션 생성함수는 액션 객체를 return
한다. 이때 createBucket(bucket)
에서 bucket
은 새로운 데이터가 된다.
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case "bucket/CREATE": {
const new_bucket_list = [...state.list, action.bucket];
return {list : new_bucket_list};
}
빈 딕셔너리가 아닌 초기 상태값을 갖고 있으므로 state
에 initialState
를 넣어준다.
switch
에 (action.type)
이 들어가 있으므로 case
의 action.type
이 무엇인지도 적어줘야 한다.
switch/case문에서 return
해주는 값이 새로운 state
가 될 것이다. 즉, return
은 기존의 리스트+새로 추가한 리스트가 들어와야 한다. spread 연산자를 사용하면 두 리스트를 한 컴포넌트 안에 나란히 출력할 수 있다.
configStore.js
import {createStore, combineReducers} from "redux";
import bucket from "./modules/bucket";
const rootReducer = combineReducers({bucket});
const store = createStore(rootReducer);
export default store;
rootReducer
: 리듀서들을 하나로 묶어주는 것combineReducer
: rootReducer
와 그 외 필요한 옵션들을 같이 묶어주는 것combineReducers({bucket, bucket2, bucket3});
이런식으로 중괄호 안에 넣어주면 된다.index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import store from "./redux/configStore";
ReactDOM.render(
<Provider store={store}> // configSotre에서 만든 store 주입
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
reportWebVitals();
2에서 생성한 store의 state를 사용하기 위해 컴포넌트에 리덕스를 연결한다. 이 행위를 컴포넌트에 스토어를 주입한다고 표현한다. provider
와 store
를 import 한 후 <BrowserRouter>
처럼 감싸주면 된다.
Redux Hooks
- useSelector - 데이터를 가져옴
- useDispatch - 데이터를 업데이트 함
BucketList.js
const BucketList = (props) => {
console.log(props);
const my_lists = props.list;
return (
...
);
};
기존에는App.js
에서 내려주는 props
의 list
로 map
을 돌렸지만 이번 시간에는 리덕스훅 useSelector를 사용해서 리덕스에 있는 데이터를 가져올 것이다.
import { useSelector } from "react-redux";
const BucketList = (props) => {
let history = useHistory();
const my_lists = useSelector((state) => state.bucket.list);
// useSelector 안에는 어떤 데이터를 가지고 오고 싶은지에 대한 함수가 들어가야 한다.
// 첫번째 state는 리덕스 스토어가 가진 전체 데이터를 의미한다.
// state.bucket.list는 스토어에서 bucket 안에 있는 list를 가져온다.
return (
<ListStyle>
{my_lists.map((list, index) => {
return (
<ItemStyle
className="list_item"
key={index}
onClick={() => {
history.push("/detail");
}}
>
{list}
</ItemStyle>
);
})}
</ListStyle>
);
};
리덕스에서 데이터를 잘 가져오는지 확인하고 싶다면 bucket.js
의 initialState
에 list
를 추가해보면 된다.
이제 useDispatch
를 사용해 데이터를 버킷리스트에 추가해줄 것이다. 현 상태에서는 추가하기 버튼을 눌러도 App.js
에 추가되는 것이기 때문에 리덕스 데이터로부터 가져온 버킷리스트에는 아무것도 추가되지 않기 때문이다. useDispatch
는 추가하기 버튼이 있는 App.js
에서 만든다.
App.js
.
.
.
import { useDispatch } from "react-redux";
import { createBucket } from "./redux/modules/bucket";
function App() {
const [list, setList] = React.useState(["영화관 가기", "매일 책읽기", "수영 배우기"]);
const text = React.useRef(null);
const dispatch = useDispatch();
// dispatch는 useDispatch()에서 return한 객체를 사용한다.
const addBucketList = () => {
dispatch(createBucket(text.current.value));
// 추가하기 버튼에 onClick={addBucketList} 함수가 걸려있으므로 여기에서 Dispatch 한다.
// dispatch 안에는 액션 객체가 들어가지만 객체를 일일히 적기 번거로우므로 대신 액션생성함수를 입력한다.
// 함수를 바로 실행하기 위해 소괄호()를 입력한다. 이때 text.current.value는 새로 추가할 bucket 데이터 즉. input에 타이핑하는 데이터다.
};
return (
.
.
.
<Input>
<input type="text" ref={text} />
<button onClick={addBucketList}>추가하기</button>
</Input>
</div>
);
}
버킷리스트 항목 중 하나를 클릭하면 "상세페이지입니다."가 아닌 해당 버킷리스트 내용이 상세페이지에 뜨도록 해보자
BucketList.js
<ItemStyle className="list_item" key={index} onClick={() => {
history.push("/detail/"+index);
}}>
{list}
</ItemStyle>
누르는 행위는 app에서 일어나지만 알아야 하는 정보는 Detail component에 있으므로 url 파라미터를 사용한다.
Detail.js
import React from "react";
import { useParams } from "react-router-dom";
.
.
.
const Detail = (props) => {
const history = useHistory();
const index = useParams();
console.log(index);
return (
<h1>상세페이지입니다.</h1>
);
}
export default Detail;
useParams
를 사용해서 url 파라미터의 index 값을 가져온다.
Detail.js
import React from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
const Detail = (props) => {
const params = useParams();
const bucket_index = params.index;
const bucket_list = useSelector((state) => state.bucket.list);
return <h1>{bucket_list[bucket_index]}</h1>;
// 상세페이지 대신 버킷리스트의 n번째 데이터를 넣어준다.
}
export default Detail;
useSelector
로 리덕스 데이터를 가져온 뒤, useParams
로 5-Ⅰ에서 알아낸 index에 맞는 버킷리스트 데이터를 가져온다.
상세페이지에 삭제버튼을 생성한 뒤, 버튼을 누르면 이전 페이지로 돌아가고 데이터를 삭제되게 해보자.
Detail.js
import { useHistory } from "react-router-dom";
.
.
.
const Detail = (props) => {
const history = useHistory();
.
.
.
return (
<div>
...
<button onClick={() => {
console.log("휴지통");
history.goBack();
}}>🗑</button>
</div>
);
}
☝🏻이전페이지로 돌아가는 이유
삭제를 실행하면 남아있는 데이터가 없으므로 해당 페이지에 머무르지 않고 메인페이지나 이전페이지로 이동시켜주는 게 프론트엔드 개발자의 기본자세!
bucket.js
// Actions
const DELETE = "bucket/DELETE";
// Action Creators
export function deleteBucket(bucket_index){
console.log("삭제할 버킷 인덱스", bucket_index);
return {type: DELETE, bucket_index};
}
// Reducer
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case "bucket/DELETE": {
console.log("리듀서:삭제", state, action);
const new_bucket_list = state.list.filter((l, idx) => {
// state의 list 배열 중에서 bucket_index와 index 값이 같은 요소를 제외한 나머지로 새 배열 생성 -> filter 사용
// filter에는 각 요소(l)와 요소의 인덱스(idx)가 들어간다.
console.log(action.bucket_index != idx, action.bucket_index, idx);
return action.bucket_index != idx;
// return은 true/false 둘 중 하나로 나뉜다. -> 명제: 삭제할 버킷리스트(action.bucket_index)가 버킷리스트 순서(idx)와 같지 않다.
// true는 새 배열(버킷리스트)에 현재 요소가 그대로 들어간다.
// false는 현재 요소가 새 배열에서 제외 된다.
});
console.log(new_bucket_list);
return {list: new_bucket_list};
// {list: new_bucket_list}가 아닌 new_bucket_list를 return 하면 Detail 컴포넌트의 bucket_list에서 undefined 에러가 발생한다.
// bucket 모듈에서는 state(모듈 전체의 상태값)를 return 해야 하는데 new_bucket_list는 배열만을 return 하기 때문이다.
// 즉, new_bucket_list 안에는 key값(=list)이 없는 상태다.
}
default: return state;
}
}
DELETE
액션을 생성한다.
📌 JavaScript / 연산자 / 비교 연산자
Detail.js
import React from "react";
import { useParams } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux"; 👈🏻
import { deleteBucket } from "./redux/modules/bucket"; 👈🏻
import { useHistory } from "react-router-dom";
const Detail = (props) => {
const history = useHistory();
const params = useParams();
const bucket_index = params.index;
const bucket_list = useSelector((state) => state.bucket.list);
const dispatch = useDispatch(); 👈🏻
return (
<div>
<h1 onClick={() => {
props.history.push("/");
}}>{bucket_list[bucket_index]}</h1>
<button onClick={() => {
console.log("휴지통");
dispatch(deleteBucket(bucket_index));
history.goBack();
}}>🗑</button>
</div>
);
}
export default Detail;
useDispatch
를 사용해서 DELETE
를 상세페이지에 연결한다.
여기까지 하면 DELETE
는 문제 없이 작동하지만 콘솔에서는 action.bucket_index
가 문자열로 출력된다.
bucket.js
// Reducer
.
.
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case "bucket/DELETE": {
console.log("리듀서:삭제", state, action);
const new_bucket_list = state.list.filter((l, idx) => {
console.log(parseInt(action.bucket_index) != idx, parseInt(action.bucket_index), idx);
return parseInt(action.bucket_index) != idx;
});
console.log(new_bucket_list);
return {list: new_bucket_list};
}
더 깔끔한 결과를 위해 parseInt(문자를 숫자로 바꿔주는 자바스크립트 내장함수)
를 사용해서 action.bucket_index
를 숫자열로 바꿔준다. 이것을 형변환이라고 한다.