Redux_(3) 리덕스를 통한 리액트 상태 관리(Todo, Counter 구현)

임쿠쿠·2020년 11월 12일
0

리덕스

목록 보기
3/3
post-thumbnail

들어가기전에..
React에 대한 기본 지식은 생략하고 리덕스 위주로 설명했습니다.

1. UI 준비

리액트 프로젝트에서 리덕스 사용 시 가장 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 것입니다.

프레젠테이셔널 컴포넌트 : 주로 상태 관리가 이루어지지 않고, 그저 props를 받아 와서 화면에 UI를 보여 주기만 하는 컴포넌트

컨테이너 컴포넌트 : 리덕스와 연동되어 있는 컴포넌트로, 리덕스로부터 상태를 받아 오기도 하고 리덕스 스토어에 액션을 디스패치한다.

2. 액션

modules/counter.js

(1) 카운터 액션타입 정의하기

//액션 타입은 대문자로 정의하고 문자열 내용은 '모듈이름/액션 이름'과 같은 형태로 작성한다
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";

(2) 카운터 액션 생성 함수 만들기

(...)
 
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

(3) 카운터 초기 상태 및 리듀서 함수 만들기

(...)
 
// 초기 상태 및 리듀서 함수 만들기
const iniitalState = {
  number: 0,
};

// 초기 상태에 nubmer 값을 설정했으며, 리듀서 함수에는 현재 상태를 참조하여 새로운 객체를 생성해서 반환하는 코드를 작성함
// export default와 export의 차이점은 export는 여러 개를 내보낼 수 있지만 export default는 하나만 내보냄
function counter(state = iniitalState, action) {
  switch (action.type) {
    case INCREASE:
      return {
        number: state.number + 1,
      };
    case DECREASE:
      return {
        number: state.number - 1,
      };
    default:
      return state;
  }
}
export default counter;

modules/todos.js

(1) TODO 액션타입 정의하기

//액션 타입은 대문자로 정의하고 문자열 내용은 '모듈이름/액션 이름'과 같은 형태로 작성한다
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";

(2) TODO 액션 생성 함수 만들기

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

let id = 3; // insert 호출 시 1씩 더해진다. id 값은 각 todo 객체가 들고 있게 될 고윳값이다.
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,
});

(3) TODO 초기 상태 및 리듀서 함수 만들기

(...)
 
// 객체에 한 개 이상의 값이 들어가므로 불변성을 유지해 주어야 한다. spread 연산자를 활용하여 구현

const initialState = {
  input: "",
  todos: [
    {
      id: 1,
      text: "리덕스 기초 배우기",
      done: true,
    },
    {
      id: 2,
      text: "리액트와 리덕스 사용하기",
      done: false,
    },
  ],
};

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

export default todos;

3. 루트 리듀서 만들기

modules/index.js

// 리듀서를 여러 개 만들었고 추 후 createStore 함수를 사용하여 스토어를 만들 때는 리듀서를 하나만 사용해야 한다.
// 그러므로 기존에 만들었던 리듀서를 하나로 합쳐 주어야 하는데 이작업은 리덕스에서 제공하는 combineReducers라는 유틸 함수를 사용하여 처리

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

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

export default rootReducer;

4. 스토어 만들기

src/index.js

import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { composeWithDevTools } from "redux-devtools-extension";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import rootReducer from "./modules";

//스토어 만들기, redux devtools 적용하기
const store = createStore(rootReducer, composeWithDevTools());

// 스토어를 사용할 수 있도록 Provider 컴포넌트로 감싸 주기 이때 store를 props로 전달해주어야 한다.
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

serviceWorker.unregister();

5. Hooks를 사용하여 컨테이너 컴포넌트 만들기

(1) 카운터 컨테이너 만들기

containers/CounterContainer.js

import React, { useCallback } from "react";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/counter";
import { useSelector, useDispatch } from "react-redux";

// useSelector을 사용하여 counter.number값을 조회 후 Counter에게 props로 넘겨준다.
const CounterContainer = () => {
  const number = useSelector((state) => state.counter.number);
  // 컨테이너 컴포넌트에서 액션을 디스패치해야 한다면 useDispatch사용
  // useCallback으로 액션을 디스패치 하는 함수를 감싸 준다.
  const dispatch = useDispatch();
  const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
  const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
  return (
    <Counter number={number} onIncrease={onIncrease} onDecrease={onDecrease} />
  );
};

export default CounterContainer;

(2) TODO 컨테이너 만들기

containers/TodosContainer.js

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

const TodosContainer = () => {
  // useSelector 비구조화 할당
  const { input, todos } = useSelector(({ todos }) => ({
    input: todos.input,
    todos: todos.todos,
  }));
  const dispatch = useDispatch();
  const onChangeInput = useCallback((input) => dispatch(changeInput(input)), [
    dispatch,
  ]);
  const onInsert = useCallback((text) => dispatch(insert(text)), [dispatch]);
  const onToggle = useCallback((id) => dispatch(toggle(id)), [dispatch]);
  const onRemove = useCallback((id) => dispatch(remove(id)), [dispatch]);
  return (
    <Todos
      input={input}
      todos={todos}
      onChangeInput={onChangeInput}
      onInsert={onInsert}
      onToggle={onToggle}
      onRemove={onRemove}
    />
  );
};

export default TodosContainer;

6. 프레젠테이셔널 컴포넌트 만들기

components/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;

components/Todos.js

import React from "react";

const TodoItem = ({ todo, onToggle, onRemove }) => {
  return (
    <div>
      <input
        type="checkbox"
        onClick={() => onToggle(todo.id)}
        checked={todo.done}
        readOnly={true}
      />
      <span style={{ textDecoration: todo.done ? "line-through" : "none" }}>
        {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}
            key={todo.id}
            onToggle={onToggle}
            onRemove={onRemove}
          />
        ))}
      </div>
    </div>
  );
};

export default Todos;

src/App.js

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

const App = () => {
  return (
    <div>
      <CounterContainer />
      <hr />
      <TodosContainer />
    </div>
  );
};

export default App;

정리
작은 프로젝트에서 리덕스를 적용하면 오히려 위와 같이 프로젝트의 복잡도가 높아질 수 있다. 하지만 로그인 시스템 혹은 프로젝트의 규모가 커질수록 전역 상태 관리에 대한 필요성은 높아지기 때문에 리덕스 사용시 상태를 체계적으로 관리할 수 있다.


참고 - 리액트를 다루는 기술(김민준)

profile
Pay it forward

0개의 댓글