230706 비동기 통신 Axios

나윤빈·2023년 7월 6일
0

TIL

목록 보기
15/55

📌 Axios

axios란? node.js와 브라우저를 위한 Promise 기반 http 클라이언트이다. 즉 http를 이용해서 서버와 통신하기 위해 사용하는 패키지라고 볼 수 있다.

1) 설치하기

  • axios 설치
yarn add axios
  • json-server 설치
yarn add json-server

2) db.json 파일 생성

{
  "todos": [
    {
      "id": 1,
      "title": "react"
    },
    {
      "id": 2,
      "title": "node"
    },
    {
      "id": 3,
      "title": "spring"
    }
  ]
}

3) json-server 구동하기

json-server --watch db.json --port 4000

📌 GET (조회)

import "./App.css";
import { useState, useEffect } from "react";
import axios from "axios";

function App() {
  // (2) 받아온 데이터를 컴포넌트 안에서 state로 쓰기 위해 state를 만들어줌
  const [todos, setTodos] = useState(null);

  // (1) db로부터 값을 가져오기 위한 (비동기) 함수 생성
  const fetchTodos = async () => {
    // await을 써주지 않으면 promise 객제 반환
    // await를 통해 response를 받아옴
    const response = await axios.get("http://localhost:4000/todos");
    // 만들어 놓은 db는 data부분에서 찾아볼 수 있음
    // 구조분해할당으로 data를 받아옴!
    const { data } = await axios.get("http://localhost:4000/todos");

    // (3) todos에 data를 set 해줌
    // 컴포넌트 안에서 state에 db가 들어갈 수 있도록!
    setTodos(data);
  };

  useEffect(() => {
    // 최초에 마운트 될 때 db로부터 값을 가져올 것이다.
    fetchTodos();
  }, []);

  return (
    <div>
      {/* 위의 로직이 돌아갈 때까지 이 부분이 기다리지 않고 먼저 실행되기 때문에
      Cannot read properties of null과 같은 오류가 날 수 있음
      이 부분을 해결하기 위해서는 옵셔널 체이닝(?.)을 사용함! */}
      {todos?.map((item) => {
        return (
          <div key={item.id}>
            {item.id} : {item.title}
          </div>
        );
      })}
    </div>
  );
}

export default App;

🤔 옵셔널 체이닝(optinal chaining)이란?? optional chaning연산자 (?.)는 객체 내의 key에 접근할 때 그 참조가 유효한지 아닌지 직접 명시하지 않고도 접근할 수 있는 연산자이다. 만약 ?. 앞의 평가대상이 nullish ( undefined 또는 null )일 경우 평가를 멈추고 undefined를 반환한다.

📌 POST (추가)

import "./App.css";
import { useState, useEffect } from "react";
import axios from "axios";

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

  // (1) input 태그에 엮을 수 있는 state 만들기
  const [inputValue, setInputValue] = useState({
    // 초기값을 JSON 형식에 맞춰서 설정해줌
    // id가 없는 이유? JSON 방식의 데이터베이스는 id 속성을 대부분 자동으로 입력 됨
    title: "",
  });

  const fetchTodos = async () => {
    const { data } = await axios.get("http://localhost:4000/todos");
    setTodos(data);
  };

  // (4) 비동기 함수로 만들어주기
  const onSubmitHandler = async () => {
    // (5) POST 요청하기 (서버에 데이터 추가하기!)
    axios.post("http://localhost:4000/todos", inputValue);
    // (6) 새로고침을 하지 않고 바로 랜더링 될 수 있도록 하기 위해 state도 함께 변경해주기
    setTodos([...todos, inputValue])
  };

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

  return (
    <>
      <div>
        {/* INPUT 영역 */}
        {/* (3) event.preventDefault를 통해 새로고침을 막아줌 */}
        <form
          onSubmit={(event) => {
            event.preventDefault();

            // 버튼 클릭 시, Input의 입력값(state)을 이용하여 DB에 저장(POST 요청)
            onSubmitHandler();
          }}
        >
          <input
            type="text"
            // (2) value와 onChange로 입력값을 state에 담아줌
            value={inputValue.title}
            onChange={(event) =>
              setInputValue({
                // 똑같이 객체 형태로 맞춰 줌
                title: event.target.value,
              })
            }
          />
          {/* form 태그 안에 버튼은 지정하지 않아도 type="submit"
          submit의 고유 특성? 버튼을 누르면 항상 새로고침 됨 */}
          <button>추가</button>
        </form>
      </div>
      <div>
        {/* 데이터 영역 */}
        {todos?.map((item) => {
          return (
            <div key={item.id}>
              {item.id} : {item.title}
            </div>
          );
        })}
      </div>
    </>
  );
}

export default App;

📌 DELETE (삭제)

import "./App.css";
import { useState, useEffect } from "react";
import axios from "axios";

function App() {
  const [todos, setTodos] = useState(null);
  const [inputValue, setInputValue] = useState({
    title: "",
  });

  const fetchTodos = async () => {
    const { data } = await axios.get("http://localhost:4000/todos");
    setTodos(data);
  };

  const onSubmitHandler = async () => {
    axios.post("http://localhost:4000/todos", inputValue);
    setTodos([...todos, inputValue]);
  };

  // (2) 삭제 버튼 핸들링 함수 만들기
  const onDeletButtonHandler = async (id) => {
    // (3) DELET 요청으로 db에서 데이터 삭제하기
    axios.delete(`http://localhost:4000/todos/${id}`);
    // (4) state도 변경해주기
    setTodos(
      todos.filter((item) => {
        return item.id != id;
      })
    );
  };

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

  return (
    <>
      <div>
        {/* INPUT 영역 */}
        <form
          onSubmit={(event) => {
            event.preventDefault();
            onSubmitHandler();
          }}
        >
          <input
            type="text"
            value={inputValue.title}
            onChange={(event) =>
              setInputValue({
                title: event.target.value,
              })
            }
          />
          <button>추가</button>
        </form>
      </div>
      <div>
        {/* 데이터 영역 */}
        {todos?.map((item) => {
          return (
            <div key={item.id}>
              {item.id} : {item.title}
              {/* (1) 삭제 버튼 추가하기 */}
              {/* 어떤 것을 삭제할 지 알려주기 위해 삭제 함수를 호출 할 때 id값을 전달 */}
              &nbsp;
              <button onClick={() => onDeletButtonHandler(item.id)}>
                삭제
              </button>
            </div>
          );
        })}
      </div>
    </>
  );
}

export default App;

📌 PATCH (수정)

import "./App.css";
import { useState, useEffect } from "react";
import axios from "axios";

function App() {
  const [todos, setTodos] = useState(null);
  const [inputValue, setInputValue] = useState({
    title: "",
  });
  // (1) 수정할 것과 관련된 state 만들기
  const [targetId, setTargetId] = useState("");
  const [contents, setContents] = useState("");

  // 조회 함수 (GET)
  const fetchTodos = async () => {
    const { data } = await axios.get("http://localhost:4000/todos");
    setTodos(data);
  };

  // 추가 함수 (POST)
  const onSubmitHandler = async () => {
    axios.post("http://localhost:4000/todos", inputValue);
    setTodos([...todos, inputValue]);
  };

  // 삭제 함수 (DELETE)
  const onDeleteButtonHandler = async (id) => {
    axios.delete(`http://localhost:4000/todos/${id}`);
    setTodos(
      todos.filter((item) => {
        return item.id != id;
      })
    );
  };

  // (3) 수정 함수 만들기
  const onUpdateButtonHandler = async () => {
    // 수정할 내용은 객체 형태로 넣어주기
    axios.patch(`http://localhost:4000/todos/${targetId}`, {
      title: contents,
    });
    // 실시간으로 변경해주기 위해서 state도 바꿔주기
    setTodos(
      todos.map((item) => {
        if (item.id === targetId) {
          return { ...item, title: contents };
        } else {
          return item;
        }
      })
    );
  };

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

  return (
    <>
      <div>
        {/* 수정 영역 */}
        {/* (2) input 태그에 value와 onChange로 입력값을 state에 담아줌 */}
        <input
          type="text"
          placeholder="수정할 아이디"
          value={targetId}
          onChange={(event) => setTargetId(event.target.value)}
        />
        <input
          type="text"
          placeholder="수정할 내용"
          value={contents}
          onChange={(event) => setContents(event.target.value)}
        />
        {/* (4) 수정 버튼을 클릭할 때 수정 함수 호출하기 */}
        <button onClick={onUpdateButtonHandler}>수정</button>
      </div>
      <div>
        {/* INPUT 영역 */}
        <form
          onSubmit={(event) => {
            event.preventDefault();
            onSubmitHandler();
          }}
        >
          <input
            type="text"
            value={inputValue.title}
            onChange={(event) =>
              setInputValue({
                title: event.target.value,
              })
            }
          />
          <button>추가</button>
        </form>
      </div>
      <div>
        {/* 데이터 영역 */}
        {todos?.map((item) => {
          return (
            <div key={item.id}>
              {item.id} : {item.title}
              &nbsp;
              <button onClick={() => onDeleteButtonHandler(item.id)}>
                삭제
              </button>
            </div>
          );
        })}
      </div>
    </>
  );
}

export default App;

📌 최종 코드

import "./App.css";
import { useState, useEffect } from "react";
import axios from "axios";

function App() {
  const [todos, setTodos] = useState(null);
  const [inputValue, setInputValue] = useState({
    title: "",
  });
  const [targetId, setTargetId] = useState("");
  const [contents, setContents] = useState("");

  // 조회 함수 (GET)
  const fetchTodos = async () => {
    const { data } = await axios.get("http://localhost:4000/todos");
    setTodos(data);
  };

  // 추가 함수 (POST)
  const onSubmitHandler = async () => {
    axios.post("http://localhost:4000/todos", inputValue);
    setTodos([...todos, inputValue]);
  };

  // 삭제 함수 (DELETE)
  const onDeleteButtonHandler = async (id) => {
    axios.delete(`http://localhost:4000/todos/${id}`);
    setTodos(
      todos.filter((item) => {
        return item.id != id;
      })
    );
  };

  //수정 함수 (UPDATE)
  const onUpdateButtonHandler = async () => {
    axios.patch(`http://localhost:4000/todos/${targetId}`, {
      title: contents,
    });
    setTodos(
      todos.map((item) => {
        if (item.id == targetId) {
          return { ...item, title: contents };
        } else {
          return item;
        }
      })
    );
  };

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

  return (
    <>
      <div>
        {/* 수정 영역 */}
        <input
          type="text"
          placeholder="수정할 아이디"
          value={targetId}
          onChange={(event) => setTargetId(event.target.value)}
        />
        <input
          type="text"
          placeholder="수정할 내용"
          value={contents}
          onChange={(event) => setContents(event.target.value)}
        />
        <button onClick={onUpdateButtonHandler}>수정</button>
      </div>
      <div>
        {/* INPUT 영역 */}
        <form
          onSubmit={(event) => {
            event.preventDefault();
            onSubmitHandler();
          }}
        >
          <input
            type="text"
            value={inputValue.title}
            onChange={(event) =>
              setInputValue({
                title: event.target.value,
              })
            }
          />
          <button>추가</button>
        </form>
      </div>
      <div>
        {/* 데이터 영역 */}
        {todos?.map((item) => {
          return (
            <div key={item.id}>
              {item.id} : {item.title}
              &nbsp;
              <button onClick={() => onDeleteButtonHandler(item.id)}>
                삭제
              </button>
            </div>
          );
        })}
      </div>
    </>
  );
}

export default App;

🤔 왜 이런 오류가 뜨는걸까??

추가 함수(POST)가 끝난 다음에 state를 추가 해주게 되면 DB에는 id가 자동으로 입력이 되지만, state에는 id 값을 알 수가 없기 때문에 화면에 id가 자동으로 갱신되지 않고 새로고침을 하고 난 후에야 갱신됨

💡 해결하려면?

  // 추가 함수 (POST)
  const onSubmitHandler = async () => {
    axios.post("http://localhost:4000/todos", inputValue);
    // setTodos([...todos, inputValue]);
    fetchTodos()
  };

state를 추가 해주는 것이 아니라 fetchTodos() 함수를 실행하여 DB를 다시 읽어오는 방식이 더 적합할 수 있다!

📌 instance 가공하기

.env 생성하기 (환경 변수 따로 관리하기)

# 환경 정보는 따로 관리!
REACT_APP_SERVER_URL="http://localhost:4000"

src > axios > api.js 생성하기

import axios from "axios";

// .create를 통해 새로운 instance를 만들기
// 가공되지 않은 순수한 axios를 가공해주기(?)
const instance = axios.create({
  baseURL: process.env.REACT_APP_SERVER_URL,
});

export default instance;

App.jsx 수정하기

import "./App.css";
import { useState, useEffect } from "react";
// import axios from "axios"; (가공되지 않았던 axios)
// 가공한 axios? 콜하기
import api from "./axios/api";

function App() {
  const [todos, setTodos] = useState(null);
  const [inputValue, setInputValue] = useState({
    title: "",
  });
  const [targetId, setTargetId] = useState("");
  const [contents, setContents] = useState("");

  // 조회 함수 (GET)
  const fetchTodos = async () => {
    // axios 말고 api로 바꿔주기
    // baseURL를 지정해줬기 때문에 그 부분은 빼주기!
    const { data } = await api.get("/todos");
    setTodos(data);
  };

  // 추가 함수 (POST)
  const onSubmitHandler = async () => {
    await api.post("/todos", inputValue);
    // setTodos([...todos, inputValue]);
    fetchTodos();
  };

  // 삭제 함수 (DELETE)
  const onDeleteButtonHandler = async (id) => {
    api.delete(`/todos/${id}`);
    setTodos(
      todos.filter((item) => {
        return item.id !== id;
      })
    );
  };

  //수정 함수 (UPDATE)
  const onUpdateButtonHandler = async () => {
    api.patch(`/todos/${targetId}`, {
      title: contents,
    });
    setTodos(
      todos.map((item) => {
        if (item.id === targetId) {
          return { ...item, title: contents };
        } else {
          return item;
        }
      })
    );
  };

  useEffect(() => {
    fetchTodos();
  }, []);
  
  // return문 생략

가공되지 않았던 axios를 import 하는 것이 아니라 가공한 axios를 콜한다.
axios를 api로 바꿔주고 baseURL를 지정해줬기 때문에 그 부분은 빼줘도 된다.

📌 interceptor 요청과 응답 사이에 관여하기

api.js

import axios from "axios";

const instance = axios.create({
  baseURL: process.env.REACT_APP_SERVER_URL,
  // timeout? 서버에 통신을 요청했을 때 얼마나 기다릴지 정해주는 것
  // 정해진 시간 안에 오지 않으면 오류를 낼 것! 단위는 ms
  // timeout: 1,
});

// [요청].interceptors.request로 접근!
instance.interceptors.request.use(
  // 요청을 보내기 전 수행되는 함수
  function (config) {
    console.log("인터셉터 요청 성공!");
    return config;
  },

  // 오류 요청을 보내기 전 수행되는 함수
  function (error) {
    console.log("인터셉터 요청 오류!");
    return Promise.reject(error);
  }
);

//[응답].interceptors.response로 접근!
instance.interceptors.response.use(
  // 응답을 내보내기 전 수행되는 함수
  function (response) {
    console.log("인터셉터 응답 성공!");
    return response;
  },

  // 오류 응답을 내보내기 전 수행되는 함수
  function (error) {
    console.log("인터셉터 응답 오류!");
    return Promise.reject(error);
  }
);

export default instance;

interceptor를 통해

  • 요청 시 content-type 적용
  • token 등 인증 관련 로직 적용
  • 서버 응답 코드에 대한 오류 처리(controller)
  • 통신시작 및 종료에 대한 전역 상태를 관리하여 spinner, progress bar 등

다양한 것들을 구현할 수 있다.

profile
프론트엔드 개발자를 꿈꾸는

0개의 댓글