[리액트 기초반] 3주차 - 리덕스 실전
0.시작하기 전에
0-Ⅰ. 덕스(Ducks) 구조
리덕스를 사용할 때는 보통 action은 action끼리, reducer는 reducer끼리 등등 모양새대로 분리해서 작성한다. 덕스 구조는 모양새 대신 기능으로 묶어서 작성하는 구조다.
ex) 버킷리스트용 action, actionCreator, reducer를 모두 한 파일에 넣음
0-Ⅱ. 리덕스 모듈 예제
// 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)
에서 그대로 받아오는 것이다.
리덕스를 사용할 때는 보통 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)
에서 그대로 받아오는 것이다.
1.모듈 생성
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)))
}
미들웨어(데이터를 외부에서 가져와야 하는 경우 데이터를 즉시 리듀서로 넘겨줄 수가 없기 때문에 대신 중간다리를 놓아주는 역할)는 버킷리스트에서 필요가 없기에 삭제했다.
1-Ⅰ.action
const CREATE = 'bucket/CREATE';
const initialState = {
list: ["영화관 가기", "매일 책읽기", "수영 배우기", "리액트 강의 수강"]
};
액션 타입을 정해주는 부분이다. CREATE 외에는 필요 없는 기능이므로 삭제한다.
덕스구조에는 없는 내용이지만 App.js
에 있던 list
를 가져와서 initialState
라는 이름으로 초기값을 생성한다.
1-Ⅱ.actionCreator
export function createBucket(bucket) {
return {type: CREATE, bucket};
}
액션 생성함수는 액션 객체를 return
한다. 이때 createBucket(bucket)
에서 bucket
은 새로운 데이터가 된다.
1-Ⅲ.reducer
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 연산자를 사용하면 두 리스트를 한 컴포넌트 안에 나란히 출력할 수 있다.
2.스토어 생성
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});
이런식으로 중괄호 안에 넣어주면 된다.
3.리덕스-컴포넌트 연결
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>
처럼 감싸주면 된다.
4.컴포넌트에서 리덕스 데이터 사용
Redux Hooks
- useSelector - 데이터를 가져옴
- useDispatch - 데이터를 업데이트 함
4-Ⅰ.useSelector
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
를 추가해보면 된다.
4-Ⅱ.useDispatch
이제 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>
);
}
5.상세페이지에 버킷리스트 띄우기
버킷리스트 항목 중 하나를 클릭하면 "상세페이지입니다."가 아닌 해당 버킷리스트 내용이 상세페이지에 뜨도록 해보자
5-Ⅰ. 몇 번째 버킷리스트를 선택했는지 알아내기
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 값을 가져온다.
5-Ⅱ.선택한 버킷리스트 가져오기
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에 맞는 버킷리스트 데이터를 가져온다.
6.리덕스 데이터 삭제
상세페이지에 삭제버튼을 생성한 뒤, 버튼을 누르면 이전 페이지로 돌아가고 데이터를 삭제되게 해보자.
6-Ⅰ.삭제버튼 생성 및 이전페이지로 가기
Detail.js
import { useHistory } from "react-router-dom";
.
.
.
const Detail = (props) => {
const history = useHistory();
.
.
.
return (
<div>
...
<button onClick={() => {
console.log("휴지통");
history.goBack();
}}>🗑</button>
</div>
);
}
☝🏻이전페이지로 돌아가는 이유
삭제를 실행하면 남아있는 데이터가 없으므로 해당 페이지에 머무르지 않고 메인페이지나 이전페이지로 이동시켜주는 게 프론트엔드 개발자의 기본자세!
6-Ⅱ.삭제하기
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
를 숫자열로 바꿔준다. 이것을 형변환이라고 한다.
Author And Source
이 문제에 관하여([리액트 기초반] 3주차 - 리덕스 실전), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@cinephile/리액트-기초반-3주차-컴포넌트에서-리덕스-데이터-사용하기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)