공부블로그

리덕스 (with typesafe-actions) 본문

리액트/리액트 공부

리덕스 (with typesafe-actions)

떠어영 2022. 9. 19. 10:13

리덕스(Redux) 

여러분의 앱의 상태 전부는 하나의 저장소(store)안에 있는 객체 트리에 저장됩니다. 상태 트리를 변경하는 유일한 방법은 무엇이 일어날지 서술하는 객체인 액션(action)을 보내는 것 뿐입니다. 액션이 상태 트리를 어떻게 변경할지 명시하기 위해 여러분은 리듀서(reducers)를 작성해야 합니다.

 

+ typesafe-actions는 타입 정의를 쉽게 해주는 패키지다!

 

★ 리덕스에 사용되는 키워드

  • 액션 (Action) : 스토어의 상태를 바꾸고 싶을 때, 액션이란 것을 발생시켜야 리듀서가 상태를 변경. 하나의 객체로 표현되고 type을 가져야 한다.
  • 액션 생성 함수 (Action Creator) : 액션 개체를 만드는 함수. 파라미터를 받아와서 액션 객체 형태로 만들어준다. 컴포넌트에서 사용한다. 
  • 리듀서 (Reducer) : 변화를 일으키는 함수. 현재의 상태와 전달받은 액션 객체를 참고하여 새로운 상태를 만들어 반환한다.
  • 스토어 (store) : 한 애플리케이션 당 하나의 스토어를 만든다. 현재의 상태와 리듀서, 몇가지 내장함수가 들어있다.
  • 디스패치 (dispatch) : 스토어의 내장함수 중 하나. 액션을 파라미터로 받아서 해당 액션을 발생시킨다. 그러면 스토어는 리듀서 함수를 실행시켜서 해당 액션을 처리하는 로직이 있다면 새로운 상태를 만든다.

 

선언해놓은 액션 중에 하나를 디스패치가 발생시키고, 리듀서 함수가 해당 액션에 맞는 상태변화를 일으키는 거!!!

< 내가 정리한 리덕스 사용법 >

더보기

액션 타입을 정의해. (const 문자열)

액션 객체를 생성하는 함수를 선언해. 어떤 타입의 액션을 생성할 거고, 파라미터로 뭘 받는지 선언

만들어질 액션 객체들에 대한 유니언 타입을 정의(alias)해. 

스토어에서 사용할 타입도 정의해.

리듀서를 만들어. 전달받은 액션에 따라 상태를 어떻게 변경할 지 구현해.

루트 리듀서를 만들고 전체 프로젝트에서 사용할 수 있도록 컴포넌트를 감싸.

이제 컴포넌트에서 디스패치에 액션 객체를 넘기면 액션이 발생되고 리듀서가 실행돼서 상태가 바뀌는 거!!!!

 

1. 액션 타입 정의 (어떤 일이 일어날지 설명)

//Ation타입 정의 (리덕스 액션에 들어갈)
//action.type 이 string 으로 추론되지 않고 'counter/INCREASE'와 같이 실제 문자열 값으로 추론 되도록 해줍니다.
export const ADD_TODO = "todo/ADD_TODO";
export const DELETE_TODO = "todo/DELETE_TODO";

 

2.  Action-Creators API (createAction)을 사용해서 액션(객체)을 생성하는 함수 선언 (addTodo라는 함수는 ADD_TODO타입의 액션을 생성)

import {createAction} from "typesafe-actions";
//Action 생성 함수 구현 
// 첫번째 인자: Action Type, 두번째 인자: payload(액션함수의 파라미터), 세번째 인자: Action Type인데 자동으로 적용
export const addTodo = createAction(ADD_TODO)<{todo: todoItem;}>();
export const deleteTodo = createAction(DELETE_TODO)<{id: number;}>()

 

3. 선언한 액션 객체들을 Type-Helpers API (ActionType)를 사용해서 새로운 타입으로 정의

import { ActionType } from "typesafe-actions";
import * as actions from './actions'

// TodoAction 이라는 새로운 타입을 정의(alias)
// type TodoAction = PayloadAction<"todo/ADD_TODO", {todo: todoItem;}> | PayloadAction<"todo/DELETE_TODO", {key: number;}> 
export type TodoAction = ActionType<typeof actions>

 

4. 스토어에서 관리할 상태의 타입을 새롭게 정의

import { todoItem } from "../../App";

//TodoType 은 todoItem 배열
export type TodoType = { todo: Array<todoItem> }

 

5. Reducer_Creators API (createReducer)리듀서 생성 

//reducer.ts
//실질적으로 Action들이 어떤 기능을 하는지 구현!!!

import { TodoAction, TodoType } from "./types"; //정의한 타입들 불러오기
import { ADD_TODO, DELETE_TODO } from "./actions"; //정의한 액션 타입들도 불러오기
//기존에 switch/case문을 통해 작성했던 리듀서를 객체형식으로 구현
import { createReducer } from "typesafe-actions"; 

//초기상태 선언
const initialState : TodoType = {todo : []}

//Reducer함수 구현 (액션에 따라 상태를 어떻게 변경할 지 결정)
//(제네릭: 클래스나 함수에서 사용할 타입을 결정) TodoType과 TodoAction 타입을 사용 
const todo = createReducer<TodoType, TodoAction>(initialState,{  
    [ADD_TODO] : (state, action) => ({
        ...state,
        todo : [...state.todo, action.payload.todo]
    })
    ,
    [DELETE_TODO] : (state, action) => ({
        ...state,
        todo : state.todo.filter(item => item.id !== action.payload.id)
    })
})

export default todo;

 

이제, 리듀서를 프로젝트에 적용해주어야 한다!

 

6. 루트 리듀서 생성 (여러개의 리듀서를 만들고 이를 합칠 때 사용)

//index.ts 
//프로젝트에 리덕스를 적용해주기 위해서 루트 리듀서 생성

import {combineReducers} from 'redux';
//ADD_TODO와 DELETE_TODO액션이 들어올 때 상태를 변경할 수 있는 'todo'라는 리듀서
import todo from './todos/reducer';
//TodoType = {todo : Aray<todoItem>}
import { TodoType } from './todos/types';

const rootReducer = combineReducers({
  todo,
})

export default rootReducer; 

//RootState 타입 생성
export type RootState = {
  todo: TodoType;
}

 

7. 스토어를 만들고 프로젝트에 적용

//index.tsx (리액트 컴포넌트를 작성 할 때에는 .tsx 확장자를 사용한다는것 주의!!!)
//store만들기 (가장 상위 폴더에서 선해서 모든 하위 태그들이 공유 가능)
import { createStore } from 'redux'; //store생성 함수
import { Provider } from 'react-redux'; //생성된 store를 모든 태그가 공유할 수 있도록 해주는 클래스
import rootReducer from './modules' //위에서 만든 루트 리듀서

//rootReducer를 파라미터로 store생성
const store = createStore(rootReducer)

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(
  //Provider의 파라미터로 전달
  <Provider store={store}>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </Provider>
);

reportWebVitals();

 

이제 리액트 컴포넌트에서 리덕스를 사용해보자! 리덕스 값을 불러와 사용하고, 액션도 디스패치 한다.

 

Redux의 Hook 두 가지를 우선 짚고 넘어가야 한다.

  • useSelector() : 리덕스 스토어에 저장된 데이터를 추출하는 Hook
  • useDispatch() : 리덕스 스토어에 설정된 action에 대한 dispatch를 연결하는 Hook

 

8. 액션 생성함수와 dispatch를 사용하여 액션을 만들어준다. 이때 useCallback을 사용하여 함수를 재사용할 수 있도록 작성

//리덕스 접근
import {useDispatch} from 'react-redux';
import {addTodo} from './modules/todos/actions';

function Input(){

    //리덕스 스토어에 설정된 action에 대한 dispatch를 연결하는 Hook
    const dispatch = useDispatch();

    const updateTodo = React.useCallback(
        (todo: todoItem) => dispatch(addTodo({todo: todo})),[dispatch]
        //action을 감지할 때만 함수 생성, 
        //addTodo액션을 감지해서 실행
    )
   
    ...
    
    return (
        <div>
            <input value={inputText} placeholder="내용을 입력하세요" onChange={(e)=>{setInputText(e.target.value)}}/> 
            <input value={inputDate} placeholder="날짜를 입력하세요" onChange={(e)=>{setInputDate(e.target.value);}}/> 
            <button onClick={(e)=>{
                // props.addItem(item); 
                e.preventDefault();
                updateTodo(item);
                
                setInputDate(date); 
                setInputText(''); 
                let newid = id+1; 
                setid(newid)
                }}> 
            + 
            </button>
        </div>
    )
}

export default Input;

 

9. useSelector를 사용해 스토어에 접근하고 state를 변경하기 위해 useDispatch로 액션을 생성한다.

import { useSelector } from 'react-redux';
import { RootState } from './modules';
import { useDispatch } from 'react-redux';
import { deleteTodo } from './modules/todos/actions';

function App() : React.ReactElement {

  //useSelector로 store(state타입이 RootState인) 접근 
  const reduxTodoList = useSelector((state: RootState) => state.todo.todo)
  const dispatch = useDispatch();
  const deleteItem = React.useCallback(
      (id: number)=> dispatch(deleteTodo({id})) ,[dispatch]
  )

  return (
    <div className="App">
      <Input/>
      {reduxTodoList.map((val: todoItem)=>
      <div style={{display: 'flex'}} key={val.id}>
        <Item 
          id={val.id}
          title = {val.title}
          date = {val.date}
          status = {val.status}
        />
        <button onClick={(e)=> {
          console.log(val.id)
          deleteItem(val.id);
          }}>삭제</button>
      </div>
      )}
    </div>
  );
}

export default App;

 

다음 과제는.....redux thunk를 사용해서 완료한 할 일만 삭제할 수 있도록 하는 거...!! 

 

 


2022 / 12 / 14 리덕스 복습하러 다시왔다!!!

todo app을 만들면서 redux로 상태관리를 했는데, 처음에 작성한 방식이 너무 복잡하게 느껴져서 조금 더 깔끔하게 구현했다

 

저번에는 type, action, reducer파일을 각각 따로 만들었는데 이번에는 action.ts, reducer.ts파일만 만들었다.

타입 정의는 대부분 reducer파일에서 작성했다.

로직은 동일하다!

 

  1. 액션 객체를 생성하는 함수를 선언  + 필요하다면 thunk함수도 선언 
  2. 액션 타입 정의 
  3. 스토어에서 사용할 타입 정의
  4. 리듀서 작성 
  5. 루트리듀서 생성 + 스토어에서 사용하는 데이터 타입이 여러개라면 루트타입 생성
  6. Redux저장소를 생성하고 리액트 컴포넌트를 감싸준다.

 

  • 폴더구조

 

  • action. ts ( ①, ② )
import {Action, ActionType, createAction} from "typesafe-actions";
import {ThunkAction} from 'redux-thunk';
import {RootState} from '..';
import {todoItem} from "./reducer";

//Action 생성 함수 구현 
// 첫번째 인자: Action Type, 두번째 인자: payload(액션함수의 파라미터), 세번째 인자: Action Type인데 자동으로 적용
export const addTodo = createAction('ADD_TODO')<{todo: todoItem;}>();
export const changeStatus = createAction('CHANGE_STATUS')<{todo: todoItem}>();
export const deleteTodo = createAction('DELETE_TODO')<{id: number;}>();
export const modifyTodo = createAction('MODIFY_TODO')<{todo: todoItem}>();

export const deleteTodoThunk = (id: number): ThunkAction<void, RootState, null, Action> => {
    return (dispatch, getState) => {
        //id가 해당 번호인 값의 상태가 참이면 삭제 dispatch
        dispatch(deleteTodo({id : id}))
    }
}
export const changeStatusThunk = (val: todoItem) : ThunkAction<void, RootState, null, Action> => {
    const {id, title, date, isCompleted } = val
    const newItem : todoItem = { id: id, title: title, date: date, isCompleted: !val.isCompleted}

    return (dispatch, getState) => {
        dispatch(changeStatus({todo : newItem}))
    }
}

const actionTypes = { addTodo, changeStatus, deleteTodo, modifyTodo }

export type TodoAction = ActionType<typeof actionTypes>

 

  • reducer.ts ( ③, ④ )
import {TodoAction} from "./actions"; //정의한 액션 타입 불러오기
import {createReducer} from "typesafe-actions"; //기존에 switch/case문을 통해 작성했던 리듀서를 객체형식으로 구현

 export interface todoItem {
     id : number;
     title : string ;
     date : number | string ;
     isCompleted: boolean ;
 }
let map = new Map<number, todoItem>()

export interface TodoType { todo: Map<number,todoItem> }
const initialState : TodoType = { todo: map }

//리듀서 생성 <스토어 데이터 타입, 액션 타입>
const todo = createReducer<TodoType, TodoAction>(initialState,{
    ADD_TODO : (state, action) => ({
        ...state,
        todo : state.todo.set(action.payload.todo.id, action.payload.todo)
    }),
    CHANGE_STATUS : (state, action) => ({
        ...state,
        todo: state.todo.set(action.payload.todo.id, action.payload.todo)
    }),
    DELETE_TODO : (state, action) => {
        state.todo.delete(action.payload.id)
        return({...state})
    },
    MODIFY_TODO : (state, action) => ({
        ...state,
        todo: state.todo.set(action.payload.todo.id, action.payload.todo)
    })
})
export default todo

 

  • index.ts ( ⑤ )
import {combineReducers} from 'redux';
import todo, {TodoType} from './todos/reducer';

export type RootState = { //스토에서 사용한는 데이터 타입들을 정의
  todo: TodoType; //todo 데이터 타입
}

const rootReducer = combineReducers({ //루트리듀서 생성
  todo, //todo리듀서
})

export default rootReducer;

 

  • index.tsx ( ⑥ )
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

import { configureStore } from '@reduxjs/toolkit'
import { AnyAction } from 'redux'; //store생성 함수
import thunk, { ThunkDispatch } from 'redux-thunk'; 
import { Provider, useDispatch } from 'react-redux'; //생성된 store를 모든 태그가 공유할 수 있도록 해주는 클래스
import rootReducer from './store' //index.ts에서 만든 루트리듀서
import {TodoType} from "./store/todos/reducer"; //todo 데이터 타입

//store 생성
const store = configureStore({reducer: rootReducer, middleware: [thunk]});

//thunkAction
export type AppThunkDispatch = ThunkDispatch<TodoType, any, AnyAction>;
export const useAppDispatch = () => useDispatch<AppThunkDispatch>();

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(
  //Provider의 파라미터로 store 전달
  <Provider store={store}>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </Provider>
);

reportWebVitals();

'리액트 > 리액트 공부' 카테고리의 다른 글

Recoil 상태 관리 라이브러리  (0) 2022.09.20
리덕스 미들웨어 redux-thunk  (0) 2022.09.19
리액트 진짜 초급 강의  (0) 2022.04.14
리액트 Router DOM 사용하기  (0) 2021.08.01
리액트 기초 다지기  (0) 2021.07.24