[React]Redux를 이용하여 React 상태 관리

UkiUkhui·2021년 11월 21일
0

React 공부중

목록 보기
24/25

[완성코드]https://codesandbox.io/s/react-redux-silseub-bfiys?file=/src/components/Todos.js

1. UI

  • 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트 분리
  • 프레젠테이셔널 컴포넌트 : 그저 props만 받아와서 UI만 보여줌
  • 컨테이너 컴포넌트 : 리덕스와 연동되어 있는 컴포넌트. 리덕스로부터 상태를 받아오거나 스토어에 액션을 디스패치하기도 함. 프레젠테이셔널 컴포넌트에 props를 넘겨줌

component/Counter.js

import React from "react";

const Counter = ({ number, onIncrease, onDecrease }) => {
  return (
    <div>
      <h1>{number}</h1>
      <div>
        <button onClick={onIncrease}>+1</button>
        <button onClick={onDecrease}>-1</button>
      </div>
    </div>
  );
};

export default Counter;

component/TodoList.js

import React from "react";

const TodoItem = ({}) => {
  return (
    <div>
      <input type="checkbox" />
      <span>예제</span>
      <button>삭제</button>
    </div>
  );
};

const Todos = ({}) => {
  const onSubmit = (e) => {
    e.preventDefault();
  };
  return (
    <div>
      <form onSubmit={onSubmit}>
        <input />
        <button type="submit">등록</button>
      </form>
      <div>
        <TodoItem />
      </div>
    </div>
  );
};

export default Todos;

App.js

import { Counter } from './component/Counter';
import React from 'react';
import { TodoList } from './component/TodoList';

export default function App() {
  return (
    <>
      <Counter number={0} />
      <hr />
      <TodoList />
    </>
  );
}

2. 리덕스 관련 코드 작성

2.1. 구조

  • 리덕스 모듈 : 액션 타입, 액션 생성함수, 리듀서가 모두 들어있는 자바스크립트 파일

2.1.1. 일반적인 구조

  • actions, constants, reducers라는 세 개의 디렉터리를 만들고 그 안에 기능별로 파일을 하나씩 만드는 방식
  • 새로운 액션을 만들 때마다 세 종류의 파일을 모두 수정해야하는 불편함이 있음.

2.1.2. Ducks 패턴

  • 리듀서와 액션 관련 코드들을 하나의 파일에 몰아서 작성

2.2. 카운터 모듈 만들기

2.2.1. 액션 타입 정의

const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
// 액션 타입 정의

액션 타입 정의 : '모듈명/액션명'

  • 액션 타입 : 대문자+문자열
  • 문자열 내 모듈명을 넣어 액션명끼리의 충돌 방지

2.2.2. 액션 생성 함수 정의

export const increase = ()=>({type:INCREASE});
export const decrease = ()=>({type:DECREASE});
//액션 생성 함수 정의

export : 추후 이 함수를 다른 파일에서 불러와서 사용 가능하게 함.

2.2.3. 초기 상태 및 리듀서 함수 생성

const initialState = {
  number: 0,
};

function counter(state = initialState, action) {
  switch (action.type) {
    case INCREASE:
      return {
        number: state.number + 1,
      };
    case DECREASE:
      return {
        number: state.number - 1,
      };
    default:
      return state;
  }
}

export default counter;
  • 초기 상태 : number 값 세팅
  • 리듀서 함수 : 현재 상태를 참조하여 새로운 객체를 생성해 반환

    export default : 단 한개만 내보낼 수 있음.

* export vs export default

import counter from '..';
import {increase, decrease} from '..';

export const increase = ()=>({type:INCREASE});
export const decrease = ()=>({type:DECREASE});
export default counter;
  • export : 여러 개 내보낼 수 있음
  • export default : 한 개만 내보낼 수 있음.

2.3. 투두리스트 모듈 만들기

2.3.1. 액션 타입 정의

module/todoList.js

const CHANGE_INPUT='todoList/CHANGE_INPUT'; 
const INSERT = 'todoList/INSERT';
const TOGGLE = 'todoList/TOGGLE';
const REMOVE = 'todoList/REMOVE';
  • CHANGE_INPUT: 인풋값 변경
  • INSERT : 새로운 todo 등록
  • TOGGLE : todo 체크/체크 해제
  • REMOVE : todo 삭제

2.3.2. 액션 생성 함수 정의

export const changeInput= input => ({
  type: CHANGE_INPUT,
  input
});

let id = 3;
export const insert = text => ({
  type:INSERT,
  todo:{
    id:id++,
    text,
    done:false
  }
});

export const toggle = id => ({
  type:TOGGLE,
  id
});

export const remove = id => ({
  type:REMOVE,
  id
});
//액션 생성 함수 정의
  • 파라미터 외 사전에 정의된 id값 이용하여 id값에 의존
  • 액션 생성 함수 : 액션을 리턴해주는 함수임.

2.3.3. 초기 상태 및 리듀서 함수 생성

const initialState = {
  input: "",
  todos: [
    { id: 1, text: "a", done: false },
    { id: 2, text: "b", done: false }
  ]
};

function todos(state = initialState, action) {
  switch (action.type) {
    case CHANGE_INPUT:
      return {
        ...state,
        input: action.input
      };
    case INSERT:
      return {
        ...state,
        todos: state.todos.concat(action.todo)
      };
    case TOGGLE:
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id
            ? {
                ...todo,
                done: !todo.done
              }
            : todo
        )
      };
    case REMOVE:
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.id)
      };
    default:
      return state;
  }
}

export default todos;
  • 배열 변화 : 배열 내장 함수 사용
  • state 변화 : 스프레드 연산자를 통해 원본값을 건들지 않고 변경.(불변성 유지)
  • action.todo : 액션 생성 함수(insert만 해당)에서 정의한 todo
  • state.todos : 최초 정의된 initialState 객체 내 input과 todos 존재함.
  • 각 action 에 따라 생성되는 필드가 다르다.(toggle, remove 같은 경우 id, changeinput은 input 필드 생성됨)
  • 또한 각 action별로 생성된 필드는 리듀서 함수에서 action을 통해서 직접 접근이 가능해짐
  • toggle, remove 같은 경우는 state(todos)의 불변성을 위하여 직접 done 수정 및 해당 id 삭제가 불가능하므로 spread 연산자, filter를 활용하여 간접적으로 state를 수정함

2.4. 루트 리듀서 만들기

  • combineReducer : 기존에 만든 리듀서들을 하나로 만들어줌

module/index.js

import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";

const rootReducer = combineReducers({
  counter,
  todos
});

export default rootReducer;
  • 스토어를 만들 때는 하나의 리듀서만 사용할 수 있음.
  • index 파일 : 나중에 불러올 때 디렉토리 명까지만 작성해줘도 불러오기 가능함
import rootReducer from "./module";

3. 리액트 애플리케이션에 리덕스 적용하기

  • 스토어를 만들고 리액트 app에 리덕스 적용 : src/index.js 에서 이뤄짐

3.1. 스토어 만들기

src/index.js

import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import App from "./App";
import rootReducer from "./modules";

const store = createStore(rootReducer);
const rootElement = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <App />
  </StrictMode>,
  rootElement
);
  • createStore를 통해 스토어 생성
  • rootReducer를 이용하여 두 개의 리듀서(counter, todos)가 통합된 하나의 리듀서 생성

3.2. Provider 컴포넌트 활용하여 리덕스 적용하기

  • Provider 컴포넌트 : App이 Redux store에 접근 가능하게 해줌
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import App from "./App";
import rootReducer from "./modules";

const store = createStore(rootReducer);
const rootElement = document.getElementById("root");
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
);

  • 리액트 컴포넌트에서 store을 사용할 수 있게 App 컴포넌트를 Provider로 감싸줌.
  • store를 props로 전달함.
  • 리액트 앱에 리덕스를 적용하기 위해 해당 컴포넌트를 App 컴포넌트를 감싸주는 것

참고:리액트 리덕스 사용법

3.3. Redux DevTool 이용

yarn add redux-devtools-extension

src/index.js

import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import App from "./App";
import rootReducer from "./modules";

const store = createStore(rootReducer, composeWithDevTools);
const rootElement = document.getElementById("root");
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
);

  • composeWithDevTools() : 리덕스 개발자 도구 활성화
  • 리덕스 개발자 도구 생김

4. 컨테이너 컴포넌트 만들기

  • 컴포넌트에서 스토어에 접근하여 원하는 state를 받아오고, 액션도 디스패치해주는 단계
    • 컨테이너 컴포넌트 : 리덕스 스토어와 연동된 컴포넌트

4.1. CounterContainer

connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)

  • 컴포넌트를 리덕스와 연동하기 위해서는 connect 함수 사용
  • connect 사용하기 위해서 2개의 파라미터 필요 :
    1)컴포넌트와 연결할 상태
    2)액션생성함수
    -->각각의 파라미터를 함수로 만들어줘야 함.
    1) mapStateToProps : 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위한 함수.(state : 현재 스토어가 지니고 있는 상태) = 상태 연결하는 함수
    2) mapDispatchToProps : 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용 = 액션 함수 연결
    -store 내 dispatch를 파라미터로 받아옴.
  • connect 함수 호출 시 컴포넌트를 리덕스 스토어에 연동시키는 새로운 함수 반환 : 반환된 함수에 프레젠테이션 컴포넌트를 파라미터로 넣어주면 리덕스와 연동된 컴포넌트 생성됨.
const makeContainer = connect(mapStateToProps, mapDispatchToProps);
makeContainer(타깃 컴포넌트);

containers/CounterContainer.js

import { connect } from "react-redux";
import Counter from "../components/Counter";

const CounterContainer = ({ number, increase, decrease }) => {
  return (
    <Counter number={number} onIncrease={increase} onDecrease={decrease} />
  );
};

const mapStateToProps = (state) => ({
  number: state.counter.number
});

const mapDispatchToProps = (dispatch) => ({
  increase: () => {
    console.log("increase");
  },
  decrease: () => {
    console.log("decrease");
  }
});
export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
  • dispatch로 연결 : 액션 발생
import { connect } from "react-redux";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/counter";

const CounterContainer = ({ number, increase, decrease }) => {
  return (
    <Counter number={number} onIncrease={increase} onDecrease={decrease} />
  );
};

const mapStateToProps = (state) => ({
  number: state.counter.number
});

const mapDispatchToProps = (dispatch) => ({
  increase: () => {
    dispatch(increase());
  },
  decrease: () => {
    dispatch(decrease());
  }
});
export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
  • mapDispatchToProps의 dispatch는 modules/counter에서 정의한 액션 생성 함수임.

4.1.1. 익명함수로 사용 시

import { connect } from "react-redux";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/counter";

const CounterContainer = ({ number, increase, decrease }) => {
  return (
    <Counter number={number} onIncrease={increase} onDecrease={decrease} />
  );
};

export default connect(
  (state) => ({
    number: state.counter.number
  }),
  (dispatch) => ({
    increase: () => {
      dispatch(increase());
    },
    decrease: () => {
      dispatch(decrease());
    }
  })
)(CounterContainer);

4.1.2. bindActionCreators

  • 컴포넌트에서 액션을 디스패치하기 위해서 각 액션 생성함수를 호출하고 dispatch로 일일히 감싸기는 어려움
export default connect(
  (state) => ({
    number: state.counter.number,
  }),
  (dispatch) =>
    bindActionCreators(
      {
        increase,
        decrease,
      },
      dispatch,
    ),
)(CounterContainer);

4.1.3. 액션 생성 함수를 객체로 넘기기

  • mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생성함수로 이뤄진 객체로 넘김!
export default connect(
  (state) => ({
    number: state.counter.number,
  }),
  {
    increase,
    decrease,
  },
)(CounterContainer);
  • connect 함수가 내부적으로 bindActionCreators 실행

4.2. TodoListContainer 생성

container/TodosContainer

import { connect } from "react-redux";
import Todos from "../components/Todos";
import { changeInput, insert, toggle, remove } from "../modules/todos";

const TodosContainer = ({
  input,
  todos,
  changeInput,
  insert,
  toggle,
  remove
}) => {
  return (
    <Todos
      input={input}
      todos={todos}
      onChangeInput={changeInput}
      onInsert={insert}
      onToggle={toggle}
      onRemove={remove}
    />
  );
};

export default connect(
  ({ todos }) => ({
    input: todos.input,
    todos: todos.todos
  }),
  {
    changeInput,
    insert,
    toggle,
    remove
  }
)(TodosContainer);

src/App.js

import CounterContainer from "./containers/CounterContainer";
import TodosContainer from "./containers/TodosContainer";

export default function App() {
  return (
    <div>
      <CounterContainer />
      <hr />
      <TodosContainer />
    </div>
  );
}

component/Todos.js

import React from "react";

const TodoItem = ({ todo, onToggle, onRemove }) => {
  return (
    <div>
      <input type="checkbox" onClick={() => onToggle(todo.id)} />
      <span>{todo.text}</span>
      <button onClick={() => onRemove(todo.id)}>삭제</button>
    </div>
  );
};

const Todos = ({
  input, //인풋에 입력되는 값
  todos, //할 일 목록이 있는 객체
  onChangeInput,
  onInsert,
  onToggle,
  onRemove
}) => {
  const onSubmit = (e) => {
    e.preventDefault();
    onInsert(input);
    onChangeInput("");
  };

  const onChange = (e) => onChangeInput(e.target.value);

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input value={input} onChange={onChange} />
        <button type="submit">등록</button>
      </form>
      <div>
        {todos.map((todo) => (
          <TodoItem todo={todo} onToggle={onToggle} onRemove={onRemove} />
        ))}
      </div>
    </div>
  );
};

export default Todos;
profile
hello world!

0개의 댓글