밍부스29) Veni, vidi, vici (부제: redux thunk로 crud구현하기)

minji jeon·2022년 8월 7일
0

TIL_

목록 보기
45/61
post-thumbnail

항해를 시작한 이래로 두번째 위기를 맞았다.
첫 위기는 항해첫날 첫 프로젝트였고,
두번째 위기는 리덕스 thunk로 crud를 구현해야 하는 과제였다.
썽크를 공부한건 오늘이 처음은 아니었다.
저번주에 파보려고 했지만 개념은 알겠는데 차마 구현은 엄두가 나질 않았다. 그래서 나중에 공부해야지 하고 미뤘는데...
띠용.. 당장 이번주 과제로 던져진 것이다.
이젠 진짜 미룰수 없다!!
오늘 목표: 한놈만 팬다

리덕스 툴킷으로 Thunk를 구현하는 방법에 대한 자료는 생각보다 많지않고, 아직 리덕스 툴킷 응용이 능숙치 않아, 코드를 이해하는데 어려움이 조금 있었다.
thunk이자식... 결국 자정이 다되서야 목표를 이룰수 있게되었다.

시작하기 앞서 thanks to...

우선 이번과제는 서버에서 데이터를 가져와서 crud를 구현하는 것이었다.
이런경우 페이지마다 데이터를 가져와서 사용하면 굳이 라이브러리가 필요할까라는 생각을 했었다.
또한 서버에서 데이터를 불러오기 때문에 전역스테이트가 필요하지 않은 상황이라고 생각했다.
하지만
나의 이런 단순한 생각이라면 리덕스는 태어나지도 않았을 것이다.

미들웨어가 왜 필요한지부터 생각해보자

미들웨어는 dispatch() 메소드를 통해 store로 가고 있는 액션을 가로채게 된다.
미들웨어 대한 개념은 이곳에 정리해두었다.
https://velog.io/@mingdolacucudas/밍부스25-미들웨어로-날씨기능-만들기

미들웨어의 역할을 정리하자면
일반적으로 각각의 페이지에서 fetch 나 axios를 사용하여 관리하던 요청을 이친구가 한방에 정리해준다.
마치 5개이던 문을 1개로 막고 경비아저씨가 관리를 해주는 격이라고 생각하면 될거같다.
한 공간에서 서버로 왔다갔다하는 모든 데이터를 관리해주니 여러 장점들이 있다.
사용하는 데이터베이스가 여러개일시 서버부하가 걸릴수도 있고, 왔다갔다하다 오류가 날수도 이를 방지할수도 있으며,
API 요청을 하면 REQUEST 액션을 디스패치해서 로딩 아이콘을 띄우고, 요청에 대한 처리가 완료되면 결과에 따라 SUCCESS나 FAILURE 액션을 디스패치해서 결과를 업데이트 하는 방식으로 많이 활용됩니다.
만약 리턴하는 함수에서 dispatch, getState를 파라미터로 받게 한다면, 스토어의 상태에도 접근할 수 있습니다.따라서 현재의 스토어 상태의 값에 따라 액션이 dispatch될지 무시될지 정해줄 수 있는 것 입니다.
그렇다면 아직 서버를 불러오지 못했을때 즉 pending상태일때 어떤 화면을 보여줄지도 정해줄 수 있는것이다.

thunk의 기본구조부터 알아보자

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

//initialstate를 만들어주고, 기본값을 지정해준다. 
const initialState = {
  //우리가 사용할 state값을 우선 []빈배열로 설정하였다. 
  entities: [],
  loading: false,
}

//createAsyncThunk : thunk의 핵심
//비동기 작업을 처리하는 액션을 만드는 함수이다. 
//pending, fulfilled , rejected 총 3종류의 action type을 가지고 있다.
//두개의 파라미터를 갖는데, 액션타입과, 콜백함수이다.  
const getPosts = createAsyncThunk(
  //action type string
  'posts/getPosts',
  // callback function
  async (thunkAPI) => {
    const res = await fetch('url').then(
    (data) => data.json()
  )
  return res
})


export const postSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {},
  //thunk로 불러온 액션은 extrareducer에서 실행된다. 
 //extrareducer는 sliceaction에서 생성되지 않은 action을 사용할수 있게 해준다. (<=>reducer: 내부액션, 동기)
  //일반 리듀서와 다르게 액션을 자동으로 생성해주지 못하기 때문에 직접 만들어 줘야 한다.   
  extraReducers: {},
})

export const postReducer = postSlice.reducer

createSlice내에서는 스토어에서 만들어진 동기적 요청이 리듀서 객체에서 조작된다.extrareducer가 비동기적 요청을 처리하는 동안말이다.

createAsyncThunk는 프로미스 라이프사이클 액션타입을 가진다.

  1. pending: posts/getPosts/pending
  2. fulfilled: posts/getPosts/fulfilled
  3. rejected: posts/getPosts/rejected

이제 그 액션타입별로 extrareducer를 만나보자

export const postSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {},
  extraReducers: {
    // pending상태일때. 
    [getPosts.pending]: (state) => {
      state.loading = true
    },
    //fulfilled상태일때
    [getPosts.fulfilled]: (state, { payload }) => {
      state.loading = false
      // getposts 함수가 fulfilled상태일때 initialstate에서 선언된 entities의 값은 payload가 된다.
      //즉, 서버에서 받은 값이 들어오는 것이다. 
      state.entities = payload
    },
    //rejected상태일때  
    [getPosts.rejected]: (state) => {
      state.loading = false
    },
  },
})

export const postReducer = postSlice.reducer

app.js에서는 어떻게 작동할까

//getposts함수를 디스패치에 보내 실행해주면 
dispatch(getposts())
//state를 가져왔을때 서버에서 받은 데이터를 스테이트에서 가져올 수 있게된다. 
const data = useSelector((state) => {
    return state.postSlice .entities;
  

thunk의 여행지도를 만들어 진행과정을 정리해 보았다.

내방식대로 이해하자면 이런 과정을 거쳐서 화면에 보여지게 되는 것 같다.

본격적으로 crud를 구현해보자

우선 나는 json서버를 이용하여 mocking data를 만들었다. 아래와 같은 형식으로 말이다.

{
  "gaebalog": [
    {
      "id": 0,
      "nickname": "ming",
      "title": "오늘의 TIL",
      "body": "진짜진짲닞다아어ㅏㅓ너무 어렵다.",
      "img": "이미지..."
    },
    {
      "id": 5,
      "nickname": "ㅎㄹㅎ",
      "title": "ㄹㅎㄹㅎ",
      "body": "ㄹㅎㄹㅎ",
      "img": ""
    }
   ] 

1. get방식

리덕스.js

const initialState = {
  loading: false,
  posts: [],
  error: "",
};
//fetch를 사용하여 데이터를 불러왔다. 
export const fetchPosts = createAsyncThunk("post/fetchPosts", async (thunkApi) => {
  const res = await fetch("http://localhost:3001/gaebalog").then((data) =>
    data.json()
  );
  console.log(res);
  return res;
});


const postSlice = createSlice({
  name: "posts",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(fetchPosts.pending, (state) => {
      state.loading = true;
    });
    //fetchposts함수가 fulfilled상태일 경우 state값은 response로 들어온 값이된다. 
    builder.addCase(fetchPosts.fulfilled, (state, action) => {
      state.loading = false;
 state.posts = action.payload;
      state.error = "";
    });
    builder.addCase(fetchPosts.rejected, (state, action) => {
      state.loading = false;
      state.posts = [];
      state.error = action.error.message;

app.js

dispatch(fetchposts())

2. post방식

리덕스.js

//이번엔 axios로 데이터를 가져와 보았다. 
//추가할 데이터를 async의 파라미터로 넣어준다. 
export const addPost = createAsyncThunk("post/addPosts", async (logData) => {
  const response = await axios.post("http://localhost:3001/gaebalog", logData);
  return response.data;
});

const postSlice = createSlice({
  name: "posts",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(fetchPosts.fulfilled, (state, action) => {
      state.loading = false
      state.posts = action.payload;
      state.error = "";
    });
   //기존스테이트에 액션을 통해 가지고온 데이터를 추가해준다. 
    //여기서부터는 원리를 알고나니 그냥 리덕스에서 reducer를 만드는 방법과 동일하여
    //쉽게 코드를 짤 수가 있었다. 
    builder.addCase(addPost.fulfilled, (state, action) => {
      state.loading = false;
      state.posts = [...state.posts, action.payload];
      state.error = "";
    });

나는 예시와는 다르게 builder라는걸 사용하였다.
빌더는 라이프사이클 메서드의 케이스라고 보면된다.

app.js

  dispatch(addPost(logData));
//payload에 추가할 데이터를 담아서 보내준다. 

3.delete방식

export const deletePost = createAsyncThunk(
  "post/deletePosts",
  // payload로 보낸 아이디를 파라미터로 넘겨주었다. 
  async (postId) => {
    const response = await axios.delete(
      `http://localhost:3001/gaebalog/${postId}`
    );
    return postId;
  }
);
//빌더의 윗부분은 생략하였다. 
//필터함수를 통해 payload로 들어온 아이디와 다른 값들을 모두 새배열로 반환해주었다. 
builder.addCase(deletePost.fulfilled, (state, action) => {
      state.loading = false;
      state.posts = state.posts.filter((post) => post.id != action.payload);
      state.error = "";
    });

app.js

 let postId= 6
 dispatch(deletePost(postId));

4.put방식

export const updatePost = createAsyncThunk(
  "post/updatePosts",
  //변경할 데이터와 아이디를 넘겨주었다. 
  //post방식과 delete방식이 합쳐진 느낌이다. 
  async ({ logData, postId }) => {
    const response = await axios.put(
      `http://localhost:3001/gaebalog/${postId}`,
      logData
    );
    console.log(postId);
    console.log(logData);
    return { postId, logData };
  }
);
//해당아이디를 찾아서 그값을 payload로 넘져준 값으로 바꿔준다. 
 builder.addCase(updatePost.fulfilled, (state, action) => {
      state.loading = false;
      state.posts = state.posts.map((post) => {
        if (post.id === action.payload.postId) {
          return action.payload.logData;
        } else {
          return post;
        }
      });
      state.error = "";
    });
  },

app.js

 dispatch(updatePost({ postId, logData }))

이때 나는 postid대신 payload에 숫자를 넣어주어서 에러가 났었다.
파라미터 개념으로 인식해서 숫자로 보내도 받을때는 첫번째 인자로 들어오니 알아서 postid로 인식할 줄 알았다. 하지만 보내주는 값과, 받는값이 모두 일치해야 한다.


아직해결하지 못한 문제

  1. put 액션을 실행했을때 나는 해당 값을 통째로 바꿔버리는 코드를 짰다.
    근데 신기하게도 아이디는 그대로이고, 값만 바뀌었다. 마법인건가...
  2. 리듀서를 fullfiled 일때, pending 상태일때, rejected상태일 때를 모두 구현해야하는 것일까 --> 우선 fulfilled일때만 구현하여도 작동은 된다. 하지만 서버의 안정성을 위해서는 모두 구현해야 하지 않을까 하는 생각도 드는데 좀더 알아봐야겠다.
    3.오늘은 구현하는데에만 집중하였지만 라이프사이클을 나타낼때 builder말고도 다른 방법이 있는걸로 안다. 이러한 방법들에 대해서도 깊게 공부해보고 싶다.
profile
은행을 뛰쳐나와 Deep Dive in javascript

0개의 댓글