코딩/React

비동기 통신 - axios, fetch

junhub 2023. 4. 28. 00:01

# Axios

 

(1) Axios란?

공식문서에 따르면 axios 란 node.js와 브라우저를 위한 Promise 기반 http 클라이언트 라고 소개하고 있다.

다시 말해 http를 이용해서 서버와 통신하기 위해 사용하는 패키지라고 생각하면 된다. 

 

(2) Axios 설치하기

yarn add axios

 

 

# json-server 설정

 

(1) 테스트용 db.json 설정

{
  "todos": [
    {
      "id": "1",
      "title": "react"
    }
  ]
}

 

 

 

# GET

 

(1) Axios get

get은 서버의 데이터를 조회할 때 사용한다. 기본적인 사용방법은 아래와 같다.

// url에는 서버의 url이 들어가고, config에는 기타 여러가지 설정을 추가할 수 있습니다.
// config는 axios 공식문서에서 확인하세요.

axios.get(url[, config]) // GET

https://axios-http.com/kr/docs/req_config

 

요청 Config | Axios Docs

요청 Config 다음은 요청을 만드는 데 사용할 수 있는 config 옵션들 입니다. 오직 url만 필수입니다. method를 지정하지 않으면 GET방식이 기본값 입니다. { url: '/user', method: 'get', baseURL: 'https://some-domain.

axios-http.com

 

 

(2) json-server API 명세서 확인하기 

Axios를 사용해서 GET 요청 코드를 작성하기에 앞서, 어떤 방식으로 요청 해야할지는 우리가 사용하는 json-server의 방식을 알아보아야 한다.

다시 말해, Axios는 GET 요청을 할 수 있도록 도와주는 패키지일뿐이지, “어떻게 요청을 해야하지?” 와 같은 방식에 대한 확인은 우리가 사용할 API 명세서를 보아야 한다는 뜻이다. 예를 들어 GET 요청을 할 때 path variable로 해야할지, query로 보내야할지는 API를 만든 사람이 하라는대로 해야 하기 때문이다. 

json-server의 공식문서를 보면, 전체 정보나 상세 정보는 아래와 같이 path variable 로 url을 작성하면 된다. 

그리고 filter와 같은 기능을 위해서 GET 요청을 하고자할 때는 query로 보내라고 명시하고 있다. 

 

(3) 코드로 알아보기

json-server에 있는 todos를 axios를 이용해서 fetching하고 useState를 통해서 관리하는 로직이다. 

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

콘솔로 결과를 확인하니, 생성한 Todos를 정상적으로 서버에서 가져와서 state가 set 한 것을 확인할 수 있다. 


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

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

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

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

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

export default App;

위 코드에서 todos?.map()이 나닌 todos.map()을 사용하면 오류가 나면서 웹페이지가 렌더링이 되지 않는데, 그 이유는 서버로 get 요청을 해서 db를 가져오는거보다 return문 내부의 코드가 먼저 실행이 되기때문에 todos 값이 null 이 되면서 오류가 나는 것이다. 

 

따라서 todos?.map()을 사용해야되는데 여기에 사용된 '?.'는 옵셔널 체이닝이라고 하며, 

todos?.map에서 todos가 null 또는 undefined 인 경우, 프로그램이 오류를 발생시키는 것을 방지하기 위해 ?. 연산자가 사용된다.

즉, todos가 존재하는 경우에만 map() 함수를 호출하고, 존재하지 않는 경우에는 아무것도 하지 않는 것이다.


# POST

 

(1) Axios POST

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

post는 보통 서버에 데이터를 추가할 때 사용한다. 다만 post 요청에 대한 로직은 BE 개발자가 구현하는 것이기때문에 추가외에 다른 용도로 사용될 수 있지만, 보통은 클라이언트의 데이터를 body형태로 서버에 보내고자 할 때 사용한다.

 

 

(2) 코드로 알아보기

 

아래 코드는 다음과 같다.

  • 화면에 인풋과 버튼이 있고, 인풋에 어떤 값을 넣고 버튼을 클릭했을 때 onSubmitHandler 이 실행됨.
    • onSubmitHandler 함수의 목적은 todo를 body에 담아 서버로 POST 요청을 보내는 것.

 

// src/App.jsx

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

const App = () => {
  // 새롭게 생성하는 todo를 관리하는 state
  const [todo, setTodo] = useState({
    title: "",
  });

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

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

  const onSubmitHandler = async(todo) => {
		//1.  이때 todos는 [{투두하나}]임
    await axios.post("http://localhost:3001/todos", todo); // 이때 서버에 있는 todos도 [{투두하나}]임
		
		// 근데 여기서 서버 요청이 끝나고 서버는 [{투두가},{두개임}]
		
	
		setTodos([...todos, todo]) 2. <-- 만약 이게 없다면, go to useEffect
		//4. 새로고침해서 진짜 현재 서버 데이터를 받아오기전에 상태를 똑같이 동기시켜줌 
		//5. 어떻게보면 유저한테 서버에서 새로 받아온것처럼 속이는거지
		
  };

  useEffect(() => {
    fetchTodos(); //3. 새로고침해서 여기를 다시 실행해줘야 서버값이 새로 들어옴 e.g) [{투두가},{두개임}]
  }, []);

  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;

 

 

(3) 네트워크탭 확인하기

우리가 post 요청을 보냈을 때, 브라우저의 네트워크 탭에는 어떤 로그가 생기는지 확인해보자.

네트워크 쪽 개발을 할 때는 항상 브라우저에 있는 네트워크 탭을 확인하면서 개발을 진행해야 한다. 어떤 문제가 생겼을 때 이정보를 통해 디버깅을 할 수 있기 때문이다. 

 

 

1. headers

  • Request URL을 통해서 우리가 의도한 URL로 post 요청을 보냈음을 알 수 있다.
  • Request Method를 통해서 우리가 POST 메서드를 사용했음을 알 수 있다. 
  • Status Code를 통해서 201 코드를 받았고, 정상적으로 네트워크가 이루어졌음을 알 수 있다. status code는 자동으로 생성되는 것이 아니라 BE개발자가 직접 개발을 하고 설정한 code가 브라우저에게 보이게 된다. 그래서 만약 BE개발자가 구현을 해놓지 않았다면 문맥과 다른 status code가 브라우저에 보일 수 있다. 
  • 그 밖에도 request headers와 response headers 정보가 추가적으로 있다. 

 

 

2. payload

  • payload에서는 보낸 body를 확인 할 수 있다. 

 

 

3. response

  • response에서는 우리가 보낸 post에 요청에 대한 서버의 응답값을 확인할 수 있다. 이 Response 값은 자동으로 생성되는 것이 아니라, FE 개발자가 BE 개발자에게 요청한 것을 직접 개발을 해야 생기는 값이다. 우리가 사용한 json-server의 경우 POST 요청을 했을 때, 클라이언트가 보낸 body를 그대로 응답해주도록 만들어져 패키지이기 때문에 위와 같이 표시된다. 

 

 

 

 

# DELETE

 

(1) Axios delete

DELETE는 저장되어 있는 데이터를 삭제하고자 요청을 보낼 때 사용한다. 

axios.delete(url[, config])  // DELETE

 

 

(2) 코드로 알아보기

 onClickDeleteButtonHandler 와 map 을 돌린 항목별로 삭제하기 버튼을 추가해준다. 

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

  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}`);
  };

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

  return (
    <>
      <form
        onSubmit={(e) => {
          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

 

(1) Axios patch

patch는 보통 어떤 데이터를 수정하고자 서버에 요청을 보낼 때 사용하는 메서드이다. 다만, 이것은 http 환경에서 서로가 한 약속이자 문맥이기때문에, 수정을 하고자 반드시 patch, put 을 써야만 하는 것은 아니다. BE에 의해서 POST를 통해서 “수정" 이라는 기능은 충분히 만들 수 있기 때문이다. 다만 이러한 약속들을 대부분의 개발자들이 지키고 있다는 점을 인지하자. 

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

 

 

(2) 코드로 알아보기

Todo를 수정하기 위해 필요한 데이터는 2개가 있다. 수정하고자하는 Todo의 id, 그리고 수정하고자 하는 값이다.

수정하고자 하는 값은 기존에 있던 todo라는 state를 사용하면 될 것이고, id는 직접 입력을 해서 url로 넘겨주는 방식으로 구현하겠다. 

보통은 수정기능을 만들 때 직접 id를 입력받아 처리는 방식은 거의 없다. 다만, 이번 예시에서는 아주 간단한 코드로 기능을 구현하는 것이기때문에 위와 같이 처리하겠다. 

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

 

 

 

 

# .env

 

1. .env 란 무엇이고 왜 사용하며 어떻게 사용해야할까? 아래 내용음 참고하여 .env 이용해서 우리가 사용할 API 서버의 IP 또는 URL을 숨겨서 처리해보자. 

https://tooo1.tistory.com/582

 

[React] .env (환경변수 관리)

※대학생이 공부하면서 작성한 글입니다※ ※이 글은 CRA로 만들었을 시 기준입니다※ env env는 API key, port, DB 등 민감한 정보를 환경변수에 담아 관리하는 방법이다. 🛑 env 주의사항 🔴 root 폴더

tooo1.tistory.com

 

 

 

# Fetch

 

(1) 개념

Fetch는 ES6부터 도입된 Javascript 내장 라이브러리이다. Promise 기반 비동기 통신 라이브러리이다.

axios처럼 데이터를 다루기 쉽고, 내장 라이브러리이기 때문에 별도의 설치 및 import를 필요로 하지 않는다. 

fetch가 가진 단점은 아래와 같다.

  • 미지원 브라우저 존재
  • 개발자에게 불친절한 response
  • axios에 비해 부족한 기능

 

 

(2) 예시를 통해 보는 fetch와 axios의 차이

 

CASE 1. 데이터 읽어오기

 

1. fetch

const url = "https://jsonplaceholder.typicode.com/todos";

fetch(url)
.then((response) => response.json())
.then(console.log);
  • fetch().then을 한 상태여도 여전히 JSON 포맷의 응답이 아니기 때문에 response.json()을 한번 더 해주는 과정이 필요하다.
  • 따라서, fetch로 데이터를 요청하는 경우 두 개의 .then()이 필요하다.

 

2. axios

const url = "https://jsonplaceholder.typicode.com/todos";

axios.get(url).then((response) => console.log(response.data));
  • axios는 친절하게도 응답(response)을 기본적으로 JSON 포맷으로 제공한다. 단순히 response.data로만 사용하면 된다. 

 

 

CASE 2. 에러 처리

 

1. axios

const url = "https://jsonplaceholder.typicode.com/todos";

axios
  .get(url)
  .then((response) => console.log(response.data))
  .catch((err) => {
    console.log(err.message);
  });
  • axios.get()요청이 반환하는 Promise 객체가 갖고있는 상태코드가 2xx의 범위를 넘어가면 거부(reject)를 한다.
  • 따라서, 곧바로 catch() 부분을 통해 error handling이 가능하다. 예시는 아래와 같다.
const url = "https://jsonplaceholder.typicode.com/todos";

// axios 요청 로직
axios
  .get(url)
  .then((response) => console.log(response.data))
	.catch((err) => {

		// 오류 객체 내의 response가 존재한다 = 서버가 오류 응답을 주었다	
		if (err.response) {
			
	    const { status, config } = err.response;
	
			// 없는 페이지
	    if (status === 404) {
	      console.log(`${config.url} not found`);
	    }

			// 서버 오류
	    if (status === 500) {
	      console.log("Server error");
	    }
	
		// 요청이 이루어졌으나 서버에서 응답이 없었을 경우
	  } else if (err.request) {
	    console.log("Error", err.message);
		// 그 외 다른 에러
	  } else {
	    console.log("Error", err.message);
	  }
	});

 

 

3. fetch 

const url = "https://jsonplaceholder.typicode.com/todos";

fetch(url)
  .then((response) => {
    if (!response.ok) {
      throw new Error(
        `This is an HTTP error: The status is ${response.status}`
      );
    }
    return response.json();
  })
  .then(console.log)
  .catch((err) => {
    console.log(err.message);
  });

fetch의 경우, catch()가 발생하는 경우는 오직 네트워크 장애 케이스이다.

따라서 개발자가 일일히 then() 안에 모든 케이스에 대한 HTTP 에러 처리를 해야한다.