[31] 리액트 리덕스 #2

반응형

리덕스 ?

리액트에서 사용률이 높은 상태관리 라이브러리이다.

상태관련 로직들을 다른파일로 분리하여 효율적으로 관리 할수있으며 글로벌 상태 관리도 쉽게 할수있다.

Context API 와 useReducer Hook 을 사용해서 개발하는 흐름과 리덕스는 굉장히 유사 하다.

Context API 와 차이점 ?

1. 미들웨어

리덕스에는 미들웨어라는 개념이 있다.

미들웨어를 통해서 액션 객체가 리듀서에서 처리되기 전에 원하는 작업을 수행할수있도록 만들수있다.

  • 특정 조건에따라 액션이 무시됨
  • 액션을 콘솔에 출력하거나 , 서버에 로깅할수있음
  • 액션이 디스패치됐을대 이를 수정해서 리듀서에 전달할수있음
  • 특정 액션이 발생했을때 다른액션을 발생하게할수있음
  • 특정 액션이 발생했을때 특정자바스크립트 함수를 실행시킬수있다.

주로 비동기 작업을 처리할때 사용된다.

2. 유용한 함수 , Hook 사용가능

connect 함수를 사용해 리덕스 상태 또는 액션 생성함수를 컴포넌트의 props 로 받아올수있다

useSelector, useDispatch, useStore 과 같은 Hooks를 사용하면 손쉽게 상태를 조회하거나 액션을 디스패치 할 수 있음

3. 하나의 커다란 상태

모든 글로벌 상태를 하나의 커다란 상태 객체에 넣어 사용한다.

리듀서 사용할때 키워드

  1. 액션 (Action)
  2. 상태에 변화가 필요할 때 발생시킴 (객체하나로 표현)type을 필수로 그외의 값들은 개발자 마음대로 생성
  3. 액션 생성함수 (Action Creator)
  4. 컴포넌트에서 더욱 쉽게 액션을 발생시키기 위함필수 아님
  5. 리듀서 (Reducer)
  6. 변화를 일으키는 함수현재의 상태와 액션을 참조하여 새로운 상태를 반환
  7. 스토어 (Store)
  8. 한 애플리케이션당 하나의 스토어현재의 앱 상태와, 리듀서, 내장함수 포함
  9. 디스패치 (dispatch)위의 키워드들을 되새기면서 아래 코드를 보면서 감을 찾아보자.
  10. import { createStore } from 'redux'; // createStore는 스토어를 만들어주는 함수입니다. // 리액트 프로젝트에서는 단 하나의 스토어를 만듭니다. /* 리덕스에서 관리 할 상태 정의 */ const initialState = { counter: 0, text: '', list: [] }; /* 액션 타입 정의 */ // 액션 타입은 주로 대문자로 작성합니다. const INCREASE = 'INCREASE'; const DECREASE = 'DECREASE'; const CHANGE_TEXT = 'CHANGE_TEXT'; const ADD_TO_LIST = 'ADD_TO_LIST'; /* 액션 생성함수 정의 */ // 액션 생성함수는 주로 camelCase 로 작성합니다. function increase() { return { type: INCREASE // 액션 객체에는 type 값이 필수입니다. }; } // 화살표 함수로 작성하는 것이 더욱 코드가 간단하기에, // 이렇게 쓰는 것을 추천합니다. const decrease = () => ({ type: DECREASE }); const changeText = text => ({ type: CHANGE_TEXT, text // 액션안에는 type 외에 추가적인 필드를 마음대로 넣을 수 있습니다. }); const addToList = item => ({ type: ADD_TO_LIST, item }); /* 리듀서 만들기 */ // 위 액션 생성함수들을 통해 만들어진 객체들을 참조하여 // 새로운 상태를 만드는 함수를 만들어봅시다. // 주의: 리듀서에서는 불변성을 꼭 지켜줘야 합니다! function reducer(state = initialState, action) { // state 의 초깃값을 initialState 로 지정했습니다. switch (action.type) { case INCREASE: return { ...state, counter: state.counter + 1 }; case DECREASE: return { ...state, counter: state.counter - 1 }; case CHANGE_TEXT: return { ...state, text: action.text }; case ADD_TO_LIST: return { ...state, list: state.list.concat(action.item) }; default: return state; } } /* 스토어 만들기 */ const store = createStore(reducer); console.log(store.getState()); // 현재 store 안에 들어있는 상태를 조회합니다. // 스토어안에 들어있는 상태가 바뀔 때 마다 호출되는 listener 함수 const listener = () => { const state = store.getState(); console.log(state); }; const unsubscribe = store.subscribe(listener); // 구독을 해제하고 싶을 때는 unsubscribe() 를 호출하면 됩니다. // 액션들을 디스패치 해봅시다. store.dispatch(increase()); store.dispatch(decrease()); store.dispatch(changeText('안녕하세요')); store.dispatch(addToList({ id: 1, text: '와우' }));
  11. 스토어의 내장함수액션을 발생 시키는 것

사용 규칙

1. 하나의 애플리케이션 안에는 하나의 스토어

여러개의 스토어를 사용하는것도 가능하나 개발도구를 활용하지못한다.

2. 상태는 읽기전용

기존 상태를 건드리지않고 새로운상태를 생성해서 업데이트 해주는 방식으로 해주면

개발자도구를 통해 뒤로돌릴수도 , 앞으로 돌릴수도있다.

이렇게 불변성을 유지해야 되는 이유는 내부적으로 데이터가 변경되는것을 감지하기위해

shallow equality 검사를 하기때문이다 .
이를 통해 객체를 깊숙히 안쪽가지 검사하는것이 아니라 겉햙기 싫으로 비교하여 좋은성능을 유지한다.

3. 리듀서는 순수한 함수여야한다.

리듀서는 이전상태와 , 액션객체를 파라미터로 받는다

이전상태는 절대로 건들이지 않고 , 변화를 일으킨 새로운 상태 객체를 만들어서 반환한다.

똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과값을 반환해야한다.

예를 들어 new Date() 나 랜덤숫자 , 네트워크 요청같은 경우는 다른결과값을 나타내게된다 .

그러므로 리듀서에서 작업하는것이 아니라 외부에서 작업을해야하고

리듀서 미들웨어 를 사용한다.

리덕스 모듈 생성하기

리덕스 모듈이란 액션타입 , 액션 생성함수 , 리듀서 가 포함된 자바스크립트 파일을 의미한다.

아래 이제 이전에 만들었던 todo-app 을 리덕스로 리팩토링해보며 익혀보자.

먼저 모듈을 생성하기위해서 src 하위 디렉토리에 redux 를 생성하고 그하위에 modules 를 생성하자.

modules 에다 리덕스 모듈파일을 생성해보자

이때 기능별로 묶어서 생성하자.

src/redux/modules/todos.js

/* 1. 액션 타입 만들기 */
// Ducks 패턴을 따를땐 액션의 이름에 접두사를 넣어주세요.
// 이렇게 하면 다른 모듈과 액션 이름이 중복되는 것을 방지 할 수 있습니다.
const ADD_TODO = "todos/ADD_TODO";
const DELETE_TODO = "todos/DELETE_TODO";
const CHECKED_TODO = "todos/CHECKED_TODO";

/* 2. 액션 생성함수 만들기 */
let nextId = 1;
export const addTodo = (content) => ({
  type: ADD_TODO,
  todo: {
    id: nextId++,
    content,
    checked: false,
    deleted: false,
  },
});

export const deleteTodo = (id) => ({
  type: DELETE_TODO,
  id,
});

export const checkedTodo = (id) => ({
  type: CHECKED_TODO,
  id,
});

/* 3. 초기 상태 선언 */
const initialState = [];

/* 4. 리듀서 선언 */
export default function todos(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return state.concat(action.todo);
    case DELETE_TODO:
      return state.map((todo) =>
        todo.id === action.id ? { ...todo, deleted: true } : todo
      );
    case CHECKED_TODO:
      return state.map((todo) =>
        todo.id === action.id ? { ...todo, checked: !todo.checked } : todo
      );

    default:
      return state;
  }
}

todos의 관련된 리덕스 로직들을 한곳에 정의했다.

위에 코드는 크게 4가지로 나눠진다.

  1. 액션타입
  2. 액션 생성함수
  3. 초기상태
  4. 리듀서

각 해당하는 내용들은 위에 키워드 부분에서 확인할수있다.

코드를 작성할때 주석으로 먼저 작성한뒤 해당하는 로직들을 적으면 좀더 정리된상태에서 코드를 작성할수있다.

위의 코드들은 모두 유기적으로 필요한데 간단하게 액션타입을 선언한것을
어떤 파라미터를 받아올지 그것을받아서 액션 객체를 만든다
그리고 리듀서를 통해 해당 액션 type 에 맞는 로직을 처리하게된다.

만약 리덕스 모듈들이 여러개라면 각각 생성했다면 해당 모듈을

루트 리듀서라는 것을 만들어서 합치는 과정이 필요하다.

보통 이럴때 modules 디렉토리안에 index.js 를 생성한뒤 combineReducers 라는 내장함수를통해 합친다

modules/index.js

**import { combineReducers } from 'redux';**
import counter from './counter';
import todos from './todos'; 

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

export default rootReducer;

이제 해당파일을 읽어올 리덕스 스토어 를 만들어보자

리덕스 스토어는 루트 디렉토리에 index.js 에 만들겠다.

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
**import { createStore } from 'redux';**
**import rootReducer from './modules';**

**const store = createStore(rootReducer); // 스토어를 만듭니다.
console.log(store.getState()); // 스토어의 상태를 확인해봅시다.**

ReactDOM.render(<App />, document.getElementById('root'));

serviceWorker.unregister();

이것을 이제 프로젝트에 적용해보자

index.js

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { legacy_createStore as createStore } from "redux";
import rootReducer from "./redux/modules";
import { Provider } from "react-redux";

const store = createStore(rootReducer);
// createStore 취소선이 그어지는 이유
// https://velog.io/@xmun74/Q-createStore-%EC%B7%A8%EC%86%8C%EC%84%A0-%EC%99%9C-%EA%B7%B8%EC%96%B4%EC%A7%80%EB%82%98

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

자 이제 루트디렉토리에 index.js 에서 리덕스들을 하위컴포넌트에서 접근할수있도록

Provider 이라는 컴포넌트에 store를 넣어서 App을감싸주게되면

렌더링하는 어느 컴포넌트에서던지 스토어에 접근할수있다.

기능 구현

1. 프리젠테이셔널 컴포넌트 생성

프리젠테이셔널 컴포넌트란 , 리덕스 스토어에 직접접근하지않고 필요한 값, 함수를 props 로만 받아와서 사용하는 컴포넌트라한다.

주로 UI 를 선언하는것에 집중한다.

components/CreateTodo/TodoForm.js

import React, { useState } from "react";
import { applyMiddleware } from "redux";
import "./TodoForm.css";

export const TodoForm = ({ onCancel, **onCreate** }) => {
  const [createTodo, setCreateTodo] = useState("");

  const todoChangeHandler = (e) => {
    setCreateTodo(e.target.value);
  };
  // form 데이터 저장
  const submitHandler = (e) => {
    e.preventDefault();
    // 입력값이 없다면 alert
    if (!createTodo) {
      alert(" 할일을 입력해주세요.");
      return;
    }
    **// 디스패치를 통해 액션함수에 값전달**
    **onCreate(createTodo);**
    setCreateTodo("");
  };

  return (
    <form onSubmit={submitHandler}>
      <div className="create-todo__control">
        <label htmlFor="">해야할 일</label>
        <input type="text" value={createTodo} onChange={todoChangeHandler} />
      </div>
      <div className="create-todo__actions">
        <button type="button" onClick={onCancel}>
          취소
        </button>
        <button type="submit">저장</button>
      </div>
    </form>
  );
};

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

컨테이너 컴포넌트란 , 리덕스 스토어의 상태를 조회하거나 액션을 디스패치할수있는 컴포넌트이다.

해당 컴포넌트는 HTML 태그들을 사용하지않고 다른 프리젠테이셔널 컴포넌트들을 불러와서 사용한다.

components/CreateTodo/CreateTodoContainer.js

import React from "react";
import { useDispatch } from "react-redux";
import { addTodo } from "../../redux/modules/todos";
import { CreateTodo } from "./CreateTodo";

const CreateTodoContainer = () => {
  **// useDispatch 는 리덕스 스토어의 dispatch 를 함수에서 사용 할 수 있게 해주는 Hook 입니다.
  const dispatch = useDispatch();

  // 각 액션들을 디스패치하는 함수들을 만드세요
  const onCreate = (content) => dispatch(addTodo(content));

    액션을 디스패치 하는 함수를 props 으로 하위 컴포넌트에 전달한다.**
  return <CreateTodo onCreate={onCreate} />;
};

export default CreateTodoContainer;

이렇게 작성한 CounterContianer 컴포넌트를 App 컴포넌트에서 불러와 렌더링하면된다.

이런식으로 프리젠테이셔널 컴포넌트 와 컨테이너 컴포넌트를 분리해서 작성하는것은

리덕스의 창시자 Dan Abramov 가 소개하게되면서 알려지게되었다.

Presentational and Container Components

 

Presentational and Container Components

You’ll find your components much easier to reuse and reason about if you divide them into two categories.

medium.com

한번사용해봤기에 나머지 todoList를 조회하는 곳에서도 동일하게 리팩토링해보자.

components/Todo/TodoContainer.js

import React from "react";
import { Todo } from "./Todo";
**import { useDispatch, useSelector } from "react-redux";
import { checkedTodo, deleteTodo } from "../../redux/modules/todos";**

// 리덕스 사용하는 로직
const TodosContainer = () => {
  **// useDispatch 는 리덕스 스토어의 dispatch 를 함수에서 사용 할 수 있게 해주는 Hook 입니다.
  const dispatch = useDispatch();
  // useSelector는 리덕스 스토어의 상태를 조회하는 Hook입니다.
  // state의 값은 store.getState() 함수를 호출했을 때 나타나는 결과물과 동일합니다.
  const todos = useSelector((state) => state.todos);

    // 삭제 , 완료 
  const onDeleteTodo = (id) => dispatch(deleteTodo(id));
  const onCheckedTodo = (id) => dispatch(checkedTodo(id));**

  return (
    <Todo
      todos={todos}
      **onDeleteTodo={onDeleteTodo}
      onCheckedTodo={onCheckedTodo}**
    ></Todo>
  );
};

export default TodosContainer;

components/Todo/TodoItem.js

import React from "react";
import { Card } from "../UI/Card";
import "./TodoItem.css";

export const TodoItem = ({ todo, id, **onDeleteTodo, onCheckedTodo** }) => {
  //todo 삭제
  const todoRemoveHandler = () => {
    if (window.confirm(" 삭제 하시겠습니까? ")) {
      **onDeleteTodo(todo.id);**
    }
  };

  //todo 체크
  const todoCheckedHandler = () => {
    **onCheckedTodo(todo.id);**
  };
  return (
    <li>
      <Card className="todo-item">
        <div className="todo-item__desc">
          <h2 className={todo.checked ? "checked" : " "}>{todo.content}</h2>
        </div>
        <button className="todo-item__button" onClick={todoCheckedHandler}>
          완료
        </button>
        <button className="todo-item__button" onClick={todoRemoveHandler}>
          삭제
        </button>
      </Card>
    </li>
  );
};

https://github.com/jaehyun2yo/todoLIst-prac

 

GitHub - jaehyun2yo/todoLIst-prac

Contribute to jaehyun2yo/todoLIst-prac development by creating an account on GitHub.

github.com

작업중인 깃헙

 

참고했던 블로그

https://react.vlpt.us/redux/

 

6장. 리덕스 · GitBook

이번 챕터에서 알아볼 주제는 리덕스(Redux) 입니다. 리덕스는 리액트 생태계에서 가장 사용률이 높은 상태관리 라이브러리입니다. 리덕스를 사용하면 여러분이 만들게 될 컴포넌트들의 상태 관

react.vlpt.us

https://velog.io/@dolarge/React-Redux-Ducks-%ED%8C%A8%ED%84%B4

 

[React] Redux - Ducks 패턴

평소에 redux를 사용해서 상태관리를 하는데 파일구조를 이런 방식으로 action, saga, reducer로 나누어 다른 파일에서 관리했다. 하지만 하나의 기능을 수정하려고 하면, 이 기능과 관련된 여러개의

velog.io

 

반응형

'프론트엔드 > React' 카테고리의 다른 글

[33] 리액트 라우터 추가 정리  (0) 2022.12.19
[32] 리액트 라우터 v6  (0) 2022.12.16
[30] 리액트 리덕스 # 1  (0) 2022.12.13
[29] 리액트 useEffect  (0) 2022.12.09
[28]리액트 ref 와 state  (1) 2022.12.09