본문 바로가기
개발일지/redux

React에서 reduxtoolkit 사용해보기 .with AsyncThunk

by 한삐 2022. 9. 1.
728x90
// redux/modules/postSlice.js

import { createAsyncThunk,createSlice } from "@reduxjs/toolkit";

const initialState = { // 초기값, data, isLoading, error로 상태관리
    posts: [],
    isLoading: false,
    error: null,
  };
  
 const postsSlice = createSlice({
	name:"posts",
	initialState: [],
	reducers: {
		createPost: (state,action) =>{ // 액션 생성함수
			state = state.concat(action.payload); // dispatch 실행부분
		}
	}
})
export const {} = createSlice.actoins; // 액션 생성함수들을 포함해서 내보내준다. reducers 부분
export default postsSlice.reducer

 

리덕스를 그냥 사용하는 경우에는 반복적인 코드가 너무 많이 나오는 상황을 접하는 경우가 잦았는데,

툴킷은 이러한 부분을 포함한 다양한 편의성을 제공하는 도구라고 보면 좋을 것 같다.

 

 


리덕스 툴킷 사용 예제 코드

 

0. 설치

yarn add @reduxjs/toolkit react-redux

npm install @reduxjs/toolkit react-redux

 

1. 스토어 생성

// src/redux/store.js

import { configureStore } from "@reduxjs/toolkit";
import posts from "./modules/postSlice" // 만들어줄 Slice

const store = configureStore({
  reducer: {
    posts,
  },
});
export default store;

 

2. 최상위 파일(index.js, App.js 등)에 Provider import

// index.js

import 기타등등
import { Provider } from "react-redux";
import store from "./redux/store";

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

3 슬라이스 생성

// 1번의 ipmort되는 Slice
// redux/modules/postSlice.jx

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";

const initialState = { // 초기값, data, isLoading, error로 상태관리
    posts: [],
    isLoading: false,
    error: null,
  };
  
  

const postsSlice = createSlice({
	name:"posts",
	initialState: [],
	reducers: {},
    extraReducers:{
    		
    	},
	}
})

export default postsSlice.reducer

// 사용한 컴포넌트에 import하고 작동시킨다.
// SomeComponent.jsx
import {} from "경로 in postSlice"

 

이 상태에서 postSlice 위에 액션 생성 함수들을 만들어 사용할 수 있다.

 

BASE_URL은 Postman을 통해 json 서버를 이용하기 때문에 로컬 경로로 설정해줬다.

Pending/Fulfilled/Rejected는 쉽게 말해 대기중/성공/실패 로 생각해도 무방하다.

 

 

 

 

일단 간단하게 임시로 만든 데이터가 잘 들어있는지 확인해보자

 

 

 

 

db.json에 임의로 만든 데이터를 포스트맨으로 확인했을때

 

내용물이 잘 들어있는 것을볼 수 있다.

 

포스트맨 사용법

https://hanbbistory.tistory.com/54

 

redux Mock server(json-server) 사용하기 / .with POST MAN

React를 사용하면서 서버와 연결하기 전, Post Man을 통해 임시로 서버를 연결해서 React의 view가 어떻게 처리되는지 보고싶을때, 데이터가 어떻게 들어가있는지 확인하고 싶을때 사용할 수 있다. post

hanbbistory.tistory.com

 

 

이제 화면으로 보내보자

 

기본적인 html이라 이뻐보이진 않지만 데이터를 잘 받는 것을 볼 수 있다.

단, useSelector를 사용할 때, (state)=>state ... 부분은 데이터의 형태에 따라 값이 들어있는 구조가 다를 수 있기 때문에

console.log를 통해 데이터를 어떻게 받아오는지 확인하며 코드를 작성해주자

 

그럼 나머지  코드를 작성하고 간단한 CRUD를 만들어보자.

 

// postSlice.js

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";

const BASE_URL = "http://localhost:3001"


const initialState = {
  // data, isLoading, error로 상태관리
  posts: [],
  isLoading: false,
  error: null,
};

export const getPosts = createAsyncThunk(
    "GET_ALL_Posts",
    async (payload, thunkAPI) => {
      try {
        const { data } = await axios.get(`${BASE_URL}/posts`);
        return thunkAPI.fulfillWithValue(data);
      } catch (error) {
        return thunkAPI.rejectWithValue(error);
      }
    }
  );

  export const addPosts = createAsyncThunk(
    "POST_Posts",
    async (payload, thunkAPI) => {
      try {
        const { data } = await axios.post(`${BASE_URL}/posts`, payload);
        console.log("data", data);
        return thunkAPI.fulfillWithValue(data);
      } catch (errer) {
        return thunkAPI.rejectWithValue(errer);
      }
    }
  );

  export const updatePosts = createAsyncThunk(
    "UPDATAE_Posts",
    async (payload, thunkAPI) => {
      try {
        console.log(payload);
        const {data} = await axios.put(
          `${BASE_URL}/posts/${payload.id}`,
          payload
        );
        console.log("data", DataTransfer);
        return thunkAPI.fulfillWithValue(data.data);
      } catch (error) {
        return thunkAPI.rejectWithValue(error);
      }
    }
  );

  export const deletePosts = createAsyncThunk(
    "DELETE_posts",
    async (payload, thunkAPI) => {
      try {
        await axios.delete(`${BASE_URL}/posts/${payload}`);
        return thunkAPI.fulfillWithValue(payload);
      } catch (error) {
        return thunkAPI.rejectWithValue(error);
      }
    }
  );

export const postsSlice = createSlice({
  name: "posts",
  initialState,
  reducers: {},
  extraReducers: {
    /* Pending */
    [getPosts.pending]: (state, action) => {
      state.isLoading = true;
    },
    [addPosts.pending]: (state, action) => {
      state.isLoading = true;
    },
    [deletePosts.pending]: (state, action) => {
      state.isLoading = true;
    },
    /* Fulfilled */
    [getPosts.fulfilled]: (state, action) => {
      state.isLoading = false;
      console.log(action)
      state.posts = [...action.payload];
    },
    [addPosts.fulfilled]: (state, action) => {
      state.isLoading = false;
      state.posts.push(action.payload);
    },
    [updatePosts.fulfilled]: (state, action) => {
      state.isLoading = false;
      console.log(action)
      const newState = state.posts.map((item) =>
        action.meta.arg.id === item.id
          ? { ...item, content: action.meta.arg.content }
          : item
      );
      state.posts = newState;
      return state;
    },
    [deletePosts.fulfilled]: (state, action) => {
      state.isLoading = false;
      const newState = state.posts.filter(
        (item) => item.id !== action.meta.arg
      );
      state.posts = newState;
      return state;
    },
    /* Rejected */
    [getPosts.rejected]: (state, action) => {
      state.isLoading = false;
      state.error = action.payload;
    },

  },
});

export default postsSlice.reducer;

 

일단  fulfilled만 설정해주고 실행해보자

 

 

 

 

CRUD가 잘 실행되는 것을 볼 수 있다.

실제로 사용할 때는 원하는 작업을 처리하기 때문에 당연히 코드가 더 구체적으로 바뀌지만

이 글에서는 CRUD가 진행되는 것만 실행시켜보기 위해 코드를 간략하게 구성했다.

 

Fulfilled 부분을 잘 작성해놔야 state값이 정상적으로 적용되어 react 화면에 데이터를 렌더링해줄 수 있다.

 

참고로 백엔드와 협업하는 등의 경우 데이터를 받을 때, json server의 데이터 형식이 다를 수 있으니,

console.log()를 확인하면서 데이터 작업을 진행하면 좋고,

가끔 Slice.js 에서 state 값을 console로 확인할 때 Froxy 데이터로 콘솔이 찍히는 경우가 있는데,

 

그런 경우에는 아래와 같이 작성하면 console.log 데이터가 정상적으로 나오게 된다.

// psotSlice.js

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { current } from "@reduxjs/toolkit"; // 이친구를 사용해보자
import axios from "axios";

const BASE_URL = "http://localhost:3001"

// fulfilled
[getPosts.fulfilled]: (state, action) => {
      state.isLoading = false;
      console.log(current(state)) // 요놈처럼 콘솔을 찍으면 데이터가 잘 찍힌다.
      state.posts = [...action.payload];

 

 

 

AsyncThunk를 사용하는 이유

공식문서

https://redux-toolkit.js.org/api/createAsyncThunk

 

말이 어려워서 무슨 뜻인지 잘 모르겠지만, 나의 언어로 표현해보자면

위의 경우로 생각하면, dispatch를 통해 보낸 액션 함수를 Slice에서 받고, Promise객체를 반환해야 하는 콜백 함수라고 보여지며

반환된 Promise를 기반으로 LifeCycle 작업을 전달하는 Thunk 작업 생성자를 반환해 비동기 요청 LifeCycle을 처리하기 위한 표준 권장 접근 방식을 추상화한다는 것은,

 

export const addPosts = createAsyncThunk(
    "POST_Posts",
    async (payload, thunkAPI) => {
      try {
        const { data } = await axios.post(`${BASE_URL}/posts`, payload);
        console.log("data", data);
        return thunkAPI.fulfillWithValue(data);
      } catch (errer) {
        return thunkAPI.rejectWithValue(errer);
      }
    }
  );
  
  ↑↑↑ 이 부분에서 Promise를 반환해주고
  
  
  
  
 ↓↓↓↓ 이 부분에서 대기/성공/실패 로 구분해 데이터를 처리해준다
 
 //Panding
 //Fulfilled
 ...
 
 [addPosts.fulfilled]: (state, action) => {
      state.isLoading = false;
      state.posts.push(action.payload); // 게시글을 추가했을때 처리해주는 방식
    },
    
 ...
 //Rejected
 
 이런 구조에 대한 설명인 것 같다

 

 

 


전체 예제코드

// Home.jsx

import { deletePosts, getPosts, updatePosts,addPosts } from "../redux/modules/postSlice";

import { useState,useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";

export default function Home() {
const [inputValue, setInputValue] = useState();

  const data = useSelector((state) => state.posts.posts);
  const dispatch = useDispatch();
  useEffect(() => {
    dispatch(getPosts());
  }, []);
  console.log(data);

  const addButton = ()=>{
    let addData = {
        nickname:"한삐",
        content:inputValue
    }
    dispatch(addPosts(addData))
  }

  const deletButton = (props) => {
    dispatch(deletePosts(props));
  };

  const editButton = (props) => {
    let updateData = {
      id: props,
      nickname: "한삐",
      content: "냠냠1231241214",
    };
    dispatch(updatePosts(updateData));
  };

  return (
    <div>
      <p>redux랑 포스팅CRUD</p>
      <p>데이터를 주세욥</p>
      <input onChange={(e)=>setInputValue(e.target.value)} type="text" />
      <button onClick={addButton}>글쓰기</button>
      <ul>
        {data.map((item) => (
          <li key={item.id}>
            <div>
              <span>{item.id}</span>
              <br />
              <span>{item.nickname}</span>
              <br />
              <span>{item.content}</span>
              <br />
            </div>
            <div>
              <button onClick={() => editButton(item.id)}>수정</button>
              <button onClick={() => deletButton(item.id)}>삭제</button>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}

 

 

// store.js
import { configureStore } from "@reduxjs/toolkit";
import posts from "./modules/postSlice"

const store = configureStore({
    reducer:{
        posts,
    }
})

export default store

 

 

// postSlice.js

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { current } from "@reduxjs/toolkit";
import axios from "axios";

const BASE_URL = "http://localhost:3001"


const initialState = {
  // data, isLoading, error로 상태관리
  posts: [],
  isLoading: false,
  error: null,
};

export const getPosts = createAsyncThunk(
    "GET_ALL_Posts",
    async (payload, thunkAPI) => {
      try {
        const { data } = await axios.get(`${BASE_URL}/posts`);
        return thunkAPI.fulfillWithValue(data);
      } catch (error) {
        return thunkAPI.rejectWithValue(error);
      }
    }
  );

  export const addPosts = createAsyncThunk(
    "POST_Posts",
    async (payload, thunkAPI) => {
      try {
        const { data } = await axios.post(`${BASE_URL}/posts`, payload);
        console.log("data", data);
        return thunkAPI.fulfillWithValue(data);
      } catch (errer) {
        return thunkAPI.rejectWithValue(errer);
      }
    }
  );

  export const updatePosts = createAsyncThunk(
    "UPDATAE_Posts",
    async (payload, thunkAPI) => {
      try {
        console.log(payload);
        const {data} = await axios.put(
          `${BASE_URL}/posts/${payload.id}`,
          payload
        );
        console.log("data", DataTransfer);
        return thunkAPI.fulfillWithValue(data.data);
      } catch (error) {
        return thunkAPI.rejectWithValue(error);
      }
    }
  );

  export const deletePosts = createAsyncThunk(
    "DELETE_posts",
    async (payload, thunkAPI) => {
      try {
        await axios.delete(`${BASE_URL}/posts/${payload}`);
        return thunkAPI.fulfillWithValue(payload);
      } catch (error) {
        return thunkAPI.rejectWithValue(error);
      }
    }
  );

export const postsSlice = createSlice({
  name: "posts",
  initialState,
  reducers: {},
  extraReducers: {
    /* Pending */
    [getPosts.pending]: (state, action) => {
      state.isLoading = true;
    },
    [addPosts.pending]: (state, action) => {
      state.isLoading = true;
    },
    [deletePosts.pending]: (state, action) => {
      state.isLoading = true;
    },
    /* Fulfilled */
    [getPosts.fulfilled]: (state, action) => {
      state.isLoading = false;
      console.log(action)
      state.posts = [...action.payload];
    },
    [addPosts.fulfilled]: (state, action) => {
      state.isLoading = false;
      state.posts.push(action.payload);
    },
    [updatePosts.fulfilled]: (state, action) => {
      state.isLoading = false;
      console.log(action)
      const newState = state.posts.map((item) =>
        action.meta.arg.id === item.id
          ? { ...item, content: action.meta.arg.content }
          : item
      );
      state.posts = newState;
      return state;
    },
    [deletePosts.fulfilled]: (state, action) => {
      state.isLoading = false;
      const newState = state.posts.filter(
        (item) => item.id !== action.meta.arg
      );
      state.posts = newState;
      return state;
    },
    /* Rejected */
    [getPosts.rejected]: (state, action) => {
      state.isLoading = false;
      state.error = action.payload;
    },

  },
});

export default postsSlice.reducer;

 

728x90

댓글