TIL/항해산 TIL

TIL 34일차 항해 심화주차 React강의 흐름 정리 (툴킷 ,Json server, axios, Thunk, react hook

매망쩔 2023. 1. 7. 14:51

강의 학습 위주로 공부했기 때문에 에러 슈팅 느낌으로 쓰기보다는, 강의의 흐름을 요약하려고 한다.

 

심화주차는 결국  리액트를 배운 팀원끼리 힘을 합쳐서  우리팀만의 react app을 만드는 것이다.

 

1. 지난 주에 리덕스에 대해서 배웠는데, 이것보다 상대적으로 사용이 편리한 툴킷에 대해서 알아본다.

 

2. 이후 Json server에 대해서 배운다.

APP을 만들려면  결국 서버가 핊요하다.  백엔드에 대해서 배우지 않은 팀원끼리 사용하기 위한 서버 = Json server

 

제이슨 서버란? 

 

아주 간단한 DB와 API 서버를 생성해주는 패키지 입니다. 우리가 json-server를 사용하는 이유는 Backend(이하 BE)에서 실제 DB와 API Server가 구축될 때까지 Frontend(이하 FE) 개발에 임시적으로 사용할 mock data를 생성하기 위함

 

  •  json-server 설치하기
yarn add json-server
  •  json-server 시작하기
yarn json-server --watch db.json --port 3001

json server를 시작하고 yarn start도 해야함

 

배포부분은 일단 건너 뛰겠습니다

 

3. Axios를 통해서 서버에서 데이터를 받는 방법을 배움

node.js와 브라우저를 위한 Promise 기반 http 클라이언트

 

Axios는 어떻게 요청할지를 도와주는 패키지임. 어떻게 요청할 지는 API 명세서를 참고해야 함

get 예시)

Axios 설치

yarn add axios

 

 

// 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;

post하기

axios.post(url[, data[, config]])   // POST

 

patch 하기  ( 수정)

axios.patch(url[, data[, config]])  // PATCH

get이후에 post와 patch 더하기

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;

4. Thunk 

dispatch(함수) → 함수실행 → 함수안에서 dispatch(객체)

 thunk는 리덕스에서 많이 사용하고 있는 미들웨어중에 하나입니다.

thunk를 사용하면 우리가 dispatch를 할때 객체가 아닌 함수를 dispatch 할 수 있게 해줍니다. 즉 dispatch(객체) 가 아니라 dispatch(함수)를 할 수 있게 되는 것이죠!

 

미들웨어

리덕스에서 dispatch를 하면 action 이 리듀서로 전달이 되고, 리듀서는 새로운 state를 반환합니다. 근데 미들웨어를 사용하면 이 과정 사이에 우리가 하고 싶은 작업들을 넣어서 할 수 있습니다.

주로 비동기 작업을 하기 위해서 미들웨어를 많이 씀.

 

미들웨어 함수

dispatch(함수) → 함수실행 → 함수안에서 dispatch(객체)

 툴킷에서는 createAsyncThunk 라는 API를 사용해서 thunk 함수를 생성할 수 있습니다. 이 API는 함수인데, 첫번째 인자에는 Action Value, 두번째 인자에는 함수가 들어갑니다. 이 함수에 우리가 하고 싶은 작업들을 구현하면 됩니다.

 

 두번째 들어가는 함수에서도 인자를 꺼낼 수 있는데, 첫번째 인자(arg)는 이 thunk함수가 외부에서 사용되었을 때 넣은 값을 여기에서 조회할 수 있고, 두번째 인자에서는 thnuk가 제공하는 여러가지 API 기능들이 담긴 객체를 꺼낼 수 있습니다.

 

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

아랫쪽은 리듀서 부분이다. 위쪽에서 이미 있는 addNumber를 사용하기전에 createAsyncThunk로 추가 함수를 만들고 해당 함수를 실행한 후 첫 번째 인자를 통해 리듀서에 dispatch한다.

(미들웨어의 경우 __을 붙여서 이름을 지음)

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;

5. Thunk-2  서버에서 데이터 가져오기

서버에서 데이터를 가져온 후, 단순히 값을 가져오기만 하면 되는게 아니라, 

통신 진행중, 실패, 성공에 대한 케이스를 모두 상태로 관리하는 로직을 구현해야 합니다.

 

isLoading : 서버에서 todos를 가져오는 상태를 나타내는 값 입니다. 초기값은 false이고, 서버와 통신이 시작되면 true였다가 통신이 끝나면 (성공 또는 실패를 하겠죠?) 다시 false로 변경됩니다.

error : 만약 서버와의 통신이 실패한 경우 서버에서 어떤 에러 메시지를 보내줄텐데요. 그것을 담아놓는 값입니다. 초기에는 에러가 없기때문에 null로 지정했습니다.

 

대부분 서버와의 통신을 상태관리 할때는 data, isLoading, error 로 관리합니다.

// src/redux/modules/todosSlice.js

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

const initialState = {
  todos: [],
  isLoading: false,
  error: null,
};

// 우리가 추가한 Thunk 함수
export const __getTodos = createAsyncThunk(
  "getTodos",
  (payload, thunkAPI) => {}
);

export const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {},
  extraReducers: {}, // 새롭게 사용할 extraReducers를 꺼내볼까요?
});

export const {} = todosSlice.actions;
export default todosSlice.reducer;

추가 작업을 해야하기에 __getTodos로 미들웨어를 만들어야 함 (Thunk 함수)

 

해당 부분을 아래와 같이 변경하면 된다.

export const __getTodos = createAsyncThunk(
  "todos/getTodos",
  async (payload, thunkAPI) => {
    try {
      const data = await axios.get("http://localhost:3001/todos");
      console.log(data);
    } catch (error) {
      console.log(error);
    }
  }
);

1. axios.get() (함수)은 Promise를 반환합니다. 

     반환된 Promise의 fullfilled 또는 rejected된 것을 처리하기위해 async/await 을 추가

2. 요청이 성공하는 경우에 실행되는 부분과 실패했을 때 실행되어야 하는 부분을 나누기 위해 try..catch 구문을 사용

 

fulfillWithValue 

Promise에서 **resolve**된 경우, 다시 말해 네트워크 요청이 성공한 경우에 dispatch 해주는 기능을 가진 API 입니다. 그리고 인자로는 payload를 넣어줄 수 있습니다.

 

rejectWithValue

Promise가 reject 된 경우, 네트워크 요청이 실패한 경우 dispatch 해주는 기능을 가진 API 입니다. 마찬가지로 인자로 어떤 값을 넣을 수 있습니다. 필자는 catch 에서 잡아주는 error 객체를 넣었습니다.

 

(두 가지 모두 툴킷에서 제공하는 api)

 

export const __getTodos = createAsyncThunk(
  "getTodos",
  async (payload, thunkAPI) => {
    try {
      const data = await axios.get("http://localhost:3001/todos");
      console.log(data);
      console.log("todosSlice 모듈에서 위는 data아래는 data.data");
      console.log(data.data);
      return thunkAPI.fulfillWithValue(data.data);
    } catch (error) {
      console.log(error);
      return thunkAPI.rejectWithValue(error);
    }
  }
);

 

axios를 통해 data를 받고 성공시 fulfillWithValue 

'

axios를 통해 data를 받고 실패시 rejectWithValue

 

이후 extrareducer를 만들어야 한다.

Slice 내부에 있는 extraRecuders에서 아래와 같이 코드를 구현합니다. extraRecuders 에서는 아래와 같이 pending, fulfilled, rejected에 대해 각각 어떻게 새로운 state를 반환할 것인지 구현할 수 있습니다.

우리가 thunk 함수에서 thunkAPI.fulfillWithValue([data.data]) 라고 작성하면 [__getTodos.fulfilled] 이 부분으로 디스패치가 됩니다. 그래서 action을 콘솔에 찍어보면 fulfillWithValue([data.data])가 보낸 액션객체를 볼 수 있습니다. type과 payload가 있죠!


      console.log(thunkAPI.fulfillWithValue(data.data));

 

extraReducers 에 pending와 rejected fullfilled상태에 따른 리듀서 로직을 추가로 구현해줍니다.

// src/redux/modules/todosSlice.js

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

const initialState = {
  todos: [],
  isLoading: false,
  error: null,
};

export const __getTodos = createAsyncThunk(
  "todos/getTodos",
  async (payload, thunkAPI) => {
    try {
      const data = await axios.get("http://localhost:3001/todos");
      return thunkAPI.fulfillWithValue(data.data);
    } catch (error) {
      return thunkAPI.rejectWithValue(error);
    }
  }
);

export const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {},
  extraReducers: {
    [__getTodos.pending]: (state) => {
      state.isLoading = true; // 네트워크 요청이 시작되면 로딩상태를 true로 변경합니다.
    },
    [__getTodos.fulfilled]: (state, action) => {
      state.isLoading = false; // 네트워크 요청이 끝났으니, false로 변경합니다.
      state.todos = action.payload; // Store에 있는 todos에 서버에서 가져온 todos를 넣습니다.
    },
    [__getTodos.rejected]: (state, action) => {
      state.isLoading = false; // 에러가 발생했지만, 네트워크 요청이 끝났으니, false로 변경합니다.
      state.error = action.payload; // catch 된 error 객체를 state.error에 넣습니다.
    },
  },
});

export const {} = todosSlice.actions;
export default todosSlice.reducer;

모든 로직이 완료됐으니 App.js에서 useSelecter로  store를 조회하면 된다.. 이때 if문으로 각각의 상태에 따라서 화면이 다르게 표시되게 하면 된다..

// src/App.jsx

import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { __getTodos } from "./redux/modules/todosSlice";

const App = () => {
  const dispatch = useDispatch();
  const { isLoading, error, todos } = useSelector((state) => state.todos);

  useEffect(() => {
    dispatch(__getTodos());
  }, [dispatch]);

  if (isLoading) {
    return <div>로딩 중....</div>;
  }

  if (error) {
    return <div>{error.message}</div>;
  }

  return (
    <div>
      {todos.map((todo) => (
        <div key={todo.id}>{todo.title}</div>
      ))}
    </div>
  );
};

export default App;

이후 성능최적화와 custom hook

https://memang828.tistory.com/58

 

35일차 react 심화주차 강의 요약 (2)- 성능최적화 +커스텀훅

 

memang828.tistory.com