본문 바로가기
🌼 리액트 공부

리액트 심화 강의 복습 / redux-toolkit , json-server, thunk , Axios

by 따따시 2022. 12. 23.

>> 리덕스-툴킷

 

yarn add react-redux @reduxjs/toolkit

 

리덕스-툴킷에 slice 있음

(slice는 액션, 액션크리에이터, 리듀서를 동시에 함)

// src/redux/modules/counterSlice.js
// 액션벨류나 엑션크리에이터를 사용한 부분이 없어졌다(일반 리덕스랑 달리) 
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  number: 0,
};

// 리덕스툴킷의 슬라이스 : 액션벨류,엑션크리에이터,리듀서가 합쳐짐
// createSlice라는 api를 통해 만들 수 있다. 
// createSlice의 인자로는 모듈의 이름, 그 모듈의 초기상태값, 모듈의 리듀서 로직이 인자로 들어감
// 리듀서 안에 들어간 로직은 리듀서의 로직이 됨과 동시에 액션크리에이터가 된다. 
const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    //액션 벨류는, 함수의 이름을 따 자동으로 생성된다.
    // 이 함수의 이름이 액션벨류가 되고, 함수안의 로직과 액션크리에이터가 생성된다. 
    addNumber: (state, action) => {
      state.number = state.number + action.payload;
    },

    minusNumber: (state, action) => {
      state.number = state.number - action.payload;
    },
  },
});

// 액션크리에이터는 컴포넌트에서 사용하기 위해 export 하고
export const { addNumber, minusNumber } = counterSlice.actions;
// reducer 는 configStore에 등록하기 위해 export default 합니다.
export default counterSlice.reducer;

 

 

>> json-server

json-server는 db랑 api서버를 생성해주는 패키지이다.

 

yarn add json-server

 

yarn json-server --watch db.json --port 3001

// db.json을 db로 사용하고, 3001번을 서버로 쓰겠다.

// db.json 파일이 만들어져 있음

 

 

yarn start

// 리액트도 실행시켜주기

 

** json-server와 리액트는 별개이니 둘 다 각각 실행시켜줘야 한다.

 

우리가 브라우저에 url을 친다는 것은 우리가 만든 api서버에 GET요청을 하는 것이다.

 

서버쪽 터미널에 가보면,

GET /todos 302 6.080 ms - -

가 적혀있는데,

‘누군가 get요청을 했고 난 6.080만에 대답했어 라고 찍힌 것

 

<json-server 공식문서>

json-server
Get a full fake REST API with zero coding in less than 30 seconds. Latest version: 0.17.1, last published: 2 months ago. Start using json-server in your project by running `npm i json-server`. There are 295 other projects in the npm registry using json-server.
https://www.npmjs.com/package/json-server

 

Routes

Based on the previous db.json file, here are all the default routes. You can also add other routes using --routes.

Plural routes ( 이게 path variable 이라는 거 같은데)

: 정해진 아이디같은 것을 넘겨서 찾아올때는 이 방식을 사용하고
GET    /posts
GET    /posts/1
POST   /posts
PUT    /posts/1
PATCH  /posts/1
DELETE /posts/1

Singular routes

GET    /profile
POST   /profile
PUT    /profile
PATCH  /profile

Filter

Use . to access deep properties

 

// 타이틀이 json-server이고 author가 typicode인 애를 줘라 라고 요청하는 것

// 검색이나 특정한 값을 찾고 싶을때는 쿼리 방식을 사용하고
GET /posts?title=json-server&author=typicode
GET /posts?id=1&id=2
GET /comments?author.name=typicode

 

+) 서연님이 yarn server를 패키지.json에 만드셨는데

그거 실행이 안되서 여쭤보니

yarn install 하고 실행해봐라했는데, 그렇게하니까 바로 서버 작동이 되었음

 


 

>> Axios

 

—악시우스는 promise 객체를 반환한다. —> 반환된 프로미스가 잘 성공적으로 처리되었는지를 fulfilled, 안되었으면 reject라고 한다.

 

yarn add axios

 

1) GET(CRUD의 READ기능)
// src/App.js

import React, { useEffect, useState } from "react";
import axios from "axios"; // axios import 합니다.

const App = () => {
  const [todos, setTodos] = useState(null);

	// axios를 통해서 get 요청을 하는 함수를 생성합니다.
	// 비동기처리를 해야하므로 async/await 구문을 통해서 처리합니다.
  const fetchTodos = async () => {
    const { data } = await axios.get("http://localhost:3001/todos");
    setTodos(data); // 서버로부터 fetching한 데이터를 useState의 state로 set 합니다.
  };
	
	// 생성한 함수를 컴포넌트가 mount 됐을 떄 실행하기 위해 useEffect를 사용합니다.
  useEffect(() => {
		// effect 구문에 생성한 함수를 넣어 실행합니다.
    fetchTodos();
  }, []);

	// data fetching이 정상적으로 되었는지 콘솔을 통해 확인합니다.
  console.log(todos); // App.js:16
  return <div>App</div>;
};

export default App;

 

const { data } = await axios.get("http://localhost:3001/todos");

→ 악시우스의 get 메소드 인자로는 (url, config) 가 있음

 

 

>> json-server(db.json)의 데이터 받아서, set함수로 state에 넣어주는 작업

const fetchTodos = async () => {

// json서버에 데이터를 요청해서(이걸 fetching한다고 하는거같아) json서버에서 가져온 데이터를
    const { data } = await axios.get("http://localhost:3001/todos");
  //  setTodos해서 todos에 넣겠다. ()
    setTodos(data); 
  };

 

 

>> 컴포넌트가 mount가 되었을 때, fetchTodos를 실행하겠다 라고 짠 것

useEffect(() => {
    fetchTodos();
  }, []);

 

(공식문서) : config에 어떤 것들이 들어가는지

요청 Config
https://axios-http.com/kr/docs/req_config

 

2) POST(CRUD의 CREATE기능)
  • post 전체코드
    // src/App.js
    
    import React, { useEffect, useState } from "react";
    import axios from "axios"; // axios import 합니다.
    
    const App = () => {
      const [todos, setTodos] = useState(null);
      const [todo, setTodo] = useState({ title: "" });
    
      // 비동기처리를 해야하므로 async/await 구문을 통해서 처리
      // axios는 프로미스 기반이라, async, await로 한다.
      const fetchTodos = async () => {
        // json서버에 데이터를 요청해서(이걸 fetching한다고 하는거같아) json서버에서 가져온 데이터를
        const { data } = await axios.get("http://localhost:3001/todos");
        //  setTodos해서 todos에 넣겠다. ()
        setTodos(data);
      };
    
      const onSubmitHandler = (todo) => {
        axios.post("http://localhost:3001/todos", todo);
      };
    
      // 생성한 함수를 컴포넌트가 mount 됐을 떄 실행하기 위해 useEffect를 사용합니다.
      useEffect(() => {
        // effect 구문에 생성한 함수를 넣어 실행합니다.
        fetchTodos();
      }, []);
    
      // data fetching이 정상적으로 되었는지 콘솔을 통해 확인합니다.
      console.log(todos); // App.js:16
      return (
        <>
          <form
            onSubmit={(e) => {
              // 👇 submit했을 때 브라우저의 새로고침을 방지합니다.
              e.preventDefault();
              onSubmitHandler(todo);
            }}
          >
            <input
              type="text"
              onChange={(ev) => {
                const { value } = ev.target;
                setTodo({
                  ...todo,
                  title: value,
                });
              }}
            />
            <button>추가하기</button>
          </form>
          <div>
            {todos?.map((todo) => (
              <div key={todo.id}>{todo.title}</div>
            ))}
          </div>
        </>
      );
    };
    
    export default App;
const onSubmitHandler = (todo) => {
    axios.post("http://localhost:3001/todos", todo);
  };

 

위 코드는 ‘새로고침’을 누르지 않으면, 마치 새로고침한것처럼

리랜더링이 발생하지 않는데 그 이유는?

todos에 todo를 추가해서, state가 업뎃되는 상황을 안만들어줬기 때문이다.

순전히 axios로 json-server의 데이터만 넣어준 것이지, state를 건드리진 않았짜농

 

아래처럼 요렇게 추가해주면 될 것

const onSubmitHandler = (todo) => {
    axios.post("http://localhost:3001/todos", todo);
    // 위 코드까지만 작성하면, 자연스러운 '새로고침'이 되지 않을 것
    // 이유 : todos(스테이트)를 건드리지 않았기 때문에
    // 따라서 자연스러운 새로고침이 일어나게 하려면, todos를 건드려주기
    setTodos({ ...todos, todo });
  };

 

 

3) DELETE(CRUD의 DELETE기능)
const onClickDeleteButtonHandler = (todoId) => {
    axios.delete(`http://localhost:3001/todos/${todoId}`);
  };

 

4) PATCH(CRUD의 UPDATE기능)
  • 수정하기 코드스니펫 붙여넣기 전
    // src/App.js
    
    import React, { useEffect, useState } from "react";
    import axios from "axios"; // axios import 합니다.
    
    const App = () => {
      const [todos, setTodos] = useState(null);
      const [todo, setTodo] = useState({ title: "" });
    
      // 비동기처리를 해야하므로 async/await 구문을 통해서 처리
      // axios는 프로미스 기반이라, async, await로 한다.
      const fetchTodos = async () => {
        // json서버에 데이터를 요청해서(이걸 fetching한다고 하는거같아) json서버에서 가져온 데이터를
        const { data } = await axios.get("http://localhost:3001/todos");
        //  setTodos해서 todos에 넣겠다. ()
        setTodos(data);
      };
    
      const onSubmitHandler = (todo) => {
        axios.post("http://localhost:3001/todos", todo);
        // 위 코드까지만 작성하면, 자연스러운 '새로고침'이 되지 않을 것
        // 이유 : todos(스테이트)를 건드리지 않았기 때문에
        // 따라서 자연스러운 새로고침이 일어나게 하려면, todos를 건드려주기
        setTodos([...todos, todo]);
      };
    
      const onClickDeleteButtonHandler = (todoId) => {
        axios.delete(`http://localhost:3001/todos/${todoId}`);
      };
    
      // 생성한 함수를 컴포넌트가 mount 됐을 떄 실행하기 위해 useEffect를 사용합니다.
      useEffect(() => {
        // effect 구문에 생성한 함수를 넣어 실행합니다.
        fetchTodos();
      }, []);
    
      // data fetching이 정상적으로 되었는지 콘솔을 통해 확인합니다.
      console.log(todos); // App.js:16
      return (
        <>
          <form
            onSubmit={(e) => {
              // 👇 submit했을 때 브라우저의 새로고침을 방지합니다.
              e.preventDefault();
              onSubmitHandler(todo);
            }}
          >
            <input
              type="text"
              onChange={(ev) => {
                const { value } = ev.target;
                setTodo({
                  ...todo,
                  title: value,
                });
              }}
            />
            <button>추가하기</button>
          </form>
          <div>
            {todos?.map((todo) => (
              <div key={todo.id}>
                {todo.title}
                <button
                  type="button"
                  onClick={() => {
                    onClickDeleteButtonHandler(todo.id);
                  }}
                >
                  삭제하기
                </button>
              </div>
            ))}
          </div>
        </>
      );
    };
    
    export default App;
    
  • PATCH 코드스니펫 전체 복사한 것
    // src/App.jsx
    
    import React, { useEffect, useState } from "react";
    import axios from "axios";
    
    const App = () => {
      const [todo, setTodo] = useState({
        title: "",
      });
      const [todos, setTodos] = useState(null);
    
      // patch에서 사용할 id, 수정값의 state를 추가
      const [targetId, setTargetId] = useState(null);
      const [editTodo, setEditTodo] = useState({
        title: "",
      });
    
      const fetchTodos = async () => {
        const { data } = await axios.get("http://localhost:3001/todos");
        setTodos(data);
      };
    
      const onSubmitHandler = (todo) => {
        axios.post("http://localhost:3001/todos", todo);
      };
    
      const onClickDeleteButtonHandler = (todoId) => {
        axios.delete(`http://localhost:3001/todos/${todoId}`);
      };
    
      // 수정버튼 이벤트 핸들러 추가 👇
      const onClickEditButtonHandler = (todoId, edit) => {
        axios.patch(`http://localhost:3001/todos/${todoId}`, edit);
      };
    
      useEffect(() => {
        fetchTodos();
      }, []);
    
      return (
        <>
          <form
            onSubmit={(e) => {
              e.preventDefault();
              onSubmitHandler(todo);
            }}
          >
            {/* 👇 수정기능에 필요한 id, 수정값 input2개와 수정하기 버튼을 추가 */}
            <div>
              <input
                type="text"
                placeholder="수정하고싶은 Todo ID"
                onChange={(ev) => {
                  setTargetId(ev.target.value);
                }}
              />
              <input
                type="text"
                placeholder="수정값 입력"
                onChange={(ev) => {
                  setEditTodo({
                    ...editTodo,
                    title: ev.target.value,
                  });
                }}
              />
              <button
                // type='button' 을 추가해야 form의 영향에서 벗어남
                type="button"
                onClick={() => onClickEditButtonHandler(targetId, editTodo)}
              >
                수정하기
              </button>
            </div>
            <input
              type="text"
              onChange={(ev) => {
                const { value } = ev.target;
                setTodo({
                  ...todo,
                  title: value,
                });
              }}
            />
            <button>추가하기</button>
          </form>
          <div>
            {todos?.map((todo) => (
              <div key={todo.id}>
                {/* todo의 아이디를 화면에 표시 */}
                {todo.id} :{todo.title}
                <button
                  type="button"
                  onClick={() => onClickDeleteButtonHandler(todo.id)}
                >
                  삭제하기
                </button>
              </div>
            ))}
          </div>
        </>
      );
    };
    
    export default App;

 

 


 

>> 리덕스-미들웨어

— 미들웨어는 비동기 처리하려고 많이 씀

리덕스-미들웨어에서 가장 많이 쓰는게 thunk다.

 

>> thunk를 쓰면, dispatcher를 보낼때 우리는 ‘객체’를 보냈었는데, ‘함수’를 보낼 수 있게 해준다.

우리는 그 함수를 통해, 우리가 끼워넣고 싶은걸 중간에 끼워넣는 것,

그 함수안에서 다시 dispatcher가 실행될 수 있도록 만드는 것

 

⇒ thunk를 사용하려면, createAsyncThunk를 사용한다.

export const __addNumber = createAsyncThunk(
	// 첫번째 인자 : action value
  "addNumber", 
	// 두번째 인자 : 콜백함수 
  (payload, thunkAPI) => {
    setTimeout(() => {
      thunkAPI.dispatch(addNumber(payload));
    }, 3000);
  }
);

createAsyncThunk 의 첫번째 인자에는 액션벨류, 두번째 인자에는 함수를 넣어주면 된다.

 

→ 이 모듈안에 있는 thunk 함수를 컴포넌트에서 import해서 사용할 것임

 

- - - - - - - - - - - - - - - - - - — - - - - - - - — - — — — - - - - — — - - - — - - - - - -

 

원래는 addNumber를 액션크리에이터로 해서 dispatch를 했다면,

const onClickAddNumberHandler = () => {
    dispatch(addNumber(number));
  };

 

이제는 미들웨어인 thunk를 거치게 하는 것

// thunk 함수를 디스패치한다. payload는 thunk함수에 넣어주면,
  // 리덕스 모듈에서 payload로 받을 수 있다.
  const onClickAddNumberHandler = () => {
    dispatch(__addNumber(number));
  };

 

 

그 thunk는 (counterSlice.js에 있음)

export const __addNumber = createAsyncThunk(
	// 첫번째 인자 : action value
  "addNumber", 
	// 두번째 인자 : 콜백함수 
  (payload, thunkAPI) => {
    setTimeout(() => {
      thunkAPI.dispatch(addNumber(payload));
    }, 3000);
  }
);

이런 형식으로, 함수를 인자로 받는거지

 

  • - - - - - - - - - - - - - - - - — - - - - - — -

 

>> Thunk 이어서

 

>> fulfillWithValue , rejectWithValue

 

리덕스-툴킷 설치

yarn add react-redux @reduxjs/toolkit

 

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

 

 

export const __getTodos = createAsyncThunk(
  "todos/getTodos",
  async (payload, thunkAPI) => {
    try {
      const data = await axios.get("http://localhost:3001/todos");
      // fulfillWithValue : 서버에서 데이터 가져오는게 성공한 경우 dispatch해주는 것
      // 인자로는 payload를 넣어줄 수 있다. 
      // 어디로 얘네가 dispatch해준다는 걸까?
      //  --> dispatch란 ? 리듀서에 action과 payload를 전달해주는 과정임
      return thunkAPI.fulfillWithValue(data.data);
    } catch (error) {
      // rejectWithValue : 서버에서 데이터 가져오는게 실패한 경우 dispatch해주는 것
      return thunkAPI.rejectWithValue(error);
    }
  }
);

 

 

이제, 리듀서를 작성할건데

thunk함수는 리듀서에 직접 넣어주는 것이 아니기 때문에, 해당 메소드를 사용해야되는데

진행중 상태,성공,실패일때는 어떻게 동작할 지 구현을 해주어야 한다.

pending ,fulfilled , reject

export const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {},
  extraReducers: {
    [__getTodos.pending]: (state) => {
      state.isLoading = true;
    },
    [__getTodos.fulfilled]: (state, action) => {
      state.isLoading = false;
      state.todos = action.payload;
    },
    [__getTodos.rejected]: (state, action) => {
      state.isLoading = false;
      state.error = action.payload;
    },
  },
});

 

 

 

 

 

댓글