[리액트 기초반] 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 }
returnwidgetexport 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};
        }

빈 딕셔너리가 아닌 초기 상태값을 갖고 있으므로 stateinitialState를 넣어준다.
switch(action.type)이 들어가 있으므로 caseaction.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를 사용하기 위해 컴포넌트에 리덕스를 연결한다. 이 행위를 컴포넌트에 스토어를 주입한다고 표현한다. providerstore를 import 한 후 <BrowserRouter>처럼 감싸주면 된다.



4.컴포넌트에서 리덕스 데이터 사용

Redux Hooks

  • useSelector - 데이터를 가져옴
  • useDispatch - 데이터를 업데이트 함

4-Ⅰ.useSelector

BucketList.js

const BucketList = (props) => {
    console.log(props);
    const my_lists = props.list;

    return (
	...
    );
};

기존에는App.js에서 내려주는 propslistmap을 돌렸지만 이번 시간에는 리덕스훅 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.jsinitialStatelist를 추가해보면 된다.

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를 숫자열로 바꿔준다. 이것을 형변환이라고 한다.

좋은 웹페이지 즐겨찾기