6 분 소요

비동기를 다시 공부하다보니 fetch, axios, promise, async와 await, then~catch, try~catch와 같은 용어들이 혼동이 와서 이를 정리하는 글이다.

alt text

추천 공부 순서🔗


  1. 비동기 프로그래밍 정리
  2. 프로미스 객체와 메서드
  3. async와 await

  4. json-server
  5. fetch()함수로 HTTP 요청하기
  6. JSON과 비동기통신

  7. 비동기 통신 - axios와 interceptor
  8. React 비동기 데이터 처리 최적화: 로딩, 에러, 재시도 로직
  9. 비동기 프로그래밍과 REST API 호출 연습
  10. Thunder Client를 이용한 통신 연습



1. 비동기 프로그래밍

1.1 동기와 비동기

먼저 동기와 비동기의 개념부터 짚고 가자.

동기와 비동기의 특징

  • 동기
    • 작업이 순차적으로 실행 -> 코드 흐름 예측 쉬움
    • but 동시 여러 작업 수행x
  • 비동기
    • 작업이 병렬적으로 진행 -> 동시 여러 작업 수행 가능
    • but 코드의 실행 흐름을 예측하기 어려움, 작업 순서 보장x


비동기 작업의 순서를 보장하기 위해 비동기 작업을 동기적으로 표현하는 것이 필요하다.

  • 비동기 작업은 일반적으로 다른 코드와 동시에 실행되므로, 작업의 순서가 보장되지 않을 수 있다.
  • 예를 들어보자. 아래 로직에 순서가 보장되지 않으면, 예를 들어 API 요청이 완료되기 전에 렌더링이 시도되는 경우, 아직 데이터를 받을 수 없기 때문에 화면에 잘못된 정보가 표시될 수 있다.
1. API를 요청한다.
2. 응답받은 데이터를 state로 저장한다.
3. 화면에 렌더링한다.


➡️따라서 비동기 작업의 동기적 표현을 해야하는데, 이를 위해 Promise 또는 async/await를 사용한다!


1.2 비동기 프로그래밍 요약

개념 설명
Promise 비동기 작업의 결과를 나타내는 객체.
async/await Promise를 쉽게 처리할 수 있도록 하는 문법적 설탕
fetch 네트워크 요청을 수행하는 프로미스 기반 내장 API
axios 네트워크 요청을 수행하는 프로미스 기반 라이브러리, 추가 기능(인터셉터, 자동 JSON 변환 등)을 제공.
then/catch Promise의 결과를 처리하는 메소드.
try/catch async/await를 사용할 때 예외 처리를 하기 위해 사용


2. Promise vs async~ await - 비동기 작업 처리

2.1 Promise

Promise는 비동기 작업에 대한 “약속”이다.

  • 🤙 비동기 처리가 끝나면 알려줘! 그러면(then) 내가 성공(resolve) 또는 실패(reject) 객체를 반환(return) 해줄게!
  • .then().catch() 메서드를 사용하여 비동기 작업의 결과를 처리할 수 있다.
  • .then()으로 성공했을 때, .catch()로 실패했을 때 콜백을 등록한다.
  • 비동기 처리 결과(resolve(성공), reject(실패))와, 진행 상태(pending(대기), filfilled(이행), rejected(실패)) 상태를 가진다.
  • 프로미스를 연결할 때 Promise Chaining을 사용한다.
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Success!");
  }, 1000);
});

myPromise
  .then((value) => console.log(value))
  .catch((error) => console.error(error));

then~catch

  • Promise 기반의 비동기 작업을 처리할 때 사용한다.
  • .then()으로 성공했을 때 실행할 콜백을 등록하고, .catch()실패 했을 때 콜백을 등록한다.
  • 실제 작업이 끝난 다음 후속 조치를 하는 느낌에 가깝다.
myPromise
  .then((value) => console.log(value))
  .catch((error) => console.error(error));


➡️ Promise Chaining은 비동기 작업을 순차적으로 처리할 때 유용하지만, 작업이 많아지면 코드가 복잡해지고 가독성이 떨어질 수 있다. async와 await를 사용하면 이런 문제를 해결할 수 있다.

관련 포스팅


2.2 async와 await의 도입

  • Promise를 더 쉽게 사용할 수 있도록 하는 문법적 설탕(syntactic sugar)이다.
  • 콜백지옥과 프로미스 체이닝 단점을 해결해준다.

    1. 프로미스를 간결하게 만들어 줌
    2. 에러 핸들링에 유리(try…catch 구문을 사용)
    3. 에러 위치를 찾기 쉬움


  • async 함수 내에서 await 키워드를 사용하면, Promise의 결과를 기다린 후 다음 코드를 실행할 수 있어 비동기 코드를 동기 코드처럼 보이게 만들어 가독성을 높일 수 있다.
  • async 함수의 리턴 값은 무조건 Promise이며, awite 키워드를 사용하여 프로미스의 결과를 기다릴 수 있게 해준다.
  • try ~ catch를 사용해 예외 처리를 더 쉽게 할 수 있다.
useEffect(() => {
  const fetchData = async () => {
    try {
      const response = await fetch("https://api.example.com/data");
      const data = await response.json();
      console.log(data);
    } catch (error) {
      console.error("Fetching data failed", error);
    } finally {
      console.log("After all");
    }
  };

  fetchData();
}, []);


실제 작업이 끝나는 걸 기다린 다음, 다음 코드를 수행하는 느낌에 가깝다.

  • try - 시도하다
    • 비동기 호출을 시도
  • catch - 잡아내다
    • 그러다 에러를 잡아내면 여기 코드 실행
  • finally - 마지막으로
    • 성공과 실패를 가리지 않고 무조건 마지막에 실행
    • 로딩을 끝내거나 하는 용도로 쓰임

관련 포스팅



3. fetch vs axios - 네트워크 요청/응답 처리

fetchaxios프로미스(Promise)를 기반으로 설계되어 있어, 비동기 작업을 쉽게 처리할 수 있다. 또한 항상 프로미스 객체(resolve, reject)를 반환한다.

  • fetch는 네트워크 오류 시에만 예외 발생시키고 HTTP 오류에 대해선 예외 발생시키지 않는다⭐는 시킨다는 치명적인 단점이 존재한다.
  • axios는 자동 JSON 변환, 요청/응답 인터셉터, 요청 취소 등 다양한 추가 기능을 제공하여 fetch보다 자주 사용된다.


3.1 fetch

fetch는 HTTP 요청을 보내기 위해 사용하는 내장 API로 별도의 라이브러리 설치 없이 사용할 수 있다.

fetch는 비동기 방식으로 작동하지만, .then을 통해 동기적으로 작동하는 것 처럼 코드를 작성할 수 있다.

useEffect(() => {
  fetch("https://api.example.com/data")
    .then((response) => response.json())
    .then((data) => console.log(data))
    .catch((error) => console.error("Fetching data failed", error));
}, []);
  • fetch API로 데이터 가져오기
  • .then().catch()를 사용한 프로미스 처리


하지만, fetch는 아래와 같은 문제가 있다.

  1. ⭐예외 처리의 문제: 네트워크 오류 시에만 예외 발생시키고 HTTP 오류에 대해선 예외 발생시키지 않는다.
  2. 프로미스 체이닝의 가독성 문제: .then().catch()로 구성된 프로미스 체이닝이 길어지면 코드의 가독성이 떨어짐
  3. 어떤 HTTP 메서드를 사용하는지 불명확

  4. 자동 JSON 변환 없음


관련 포스팅


3.2 axios - fetch 단점 대안

axios는 HTTP 요청을 보내기 위해 사용하는 외부 라이브러리로, npm 또는 yarn을 통해 설치해야 한다.

  • fetch보다 간편하게 사용할 수 있으며, 다양한 기능을 제공한다.
    1. 자동 josn 변환
    2. 에러처리
    3. 요청 취소
    4. 요청에 대한 타임아웃 설정
    5. 인터셉터(intercepter)
      1. aixos 요청 및 응답을 가로채어 변형하는 역할을 한다.
      2. 보통 axios 인스턴스를 생성한 후, 하위에 인터셉터를 사용한다.
      3. axios 인스턴스란, 기본 설정을 미리 구성해놓은 사용자 정의 axios 인스턴스를 생성하는 것을 말한다.
import axios from "axios";

useEffect(() => {
  axios
    .get("https://api.example.com/data")
    .then((response) => console.log(response.data))
    .catch((error) => console.error("Fetching data failed", error));
}, []);

관련 포스팅



4. 비동기 호출 패턴

4.1 API로부터 받아온 데이터를 렌더링하기

API로부터 받아온 데이터를 렌더링하기 위해, 주로 useStateuseEffect 훅을 사용하여 데이터를 가져오고 상태에 저장한 다음, 데이터를 컴포넌트에 렌더링한다.

import axios from "axios";
import { useState, useEffect } from "react";

const App = () => {
  const [data, setData] = useState([]); // 1단계 : 상태 변수 생성

  useEffect(() => {
    const getTopics = async () => {
      try {
        const res = await axios.get("http://localhost:4000/topics"); // 2단계 : 데이터 가져오기
        setData(res.data); // 3단계 : 데이터 저장
      } catch (error) {
        console.error("데이터를 불러오는 중 에러가 발생했습니다.");
      }
    };
    getTopics();
  }, []); // 의존성 배열이 비어있으므로 컴포넌트가 마운트될 때 한 번만 실행

  return (
    <div>
      {/* 4단계 : 데이터 렌더링 */}
      {data.map((topic) => (
        <div key={topic.id}>
          <div>{topic.title}</div>
          <div>{topic.body}</div>
        </div>
      ))}
    </div>
  );
};

export default App;


4.2 로딩 및 에러 처리

import { useEffect, useState } from "react";
import axios from "axios";

const MainPage = () => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true); // 처음에 들어오면 로딩 활성화
  const [error, setError] = useState(null); /// 에러를 저장

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get(
          "https://jsonplaceholder.typicode.com/posts"
        );
        setData(response.data);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false); // 로딩 완료 상태로 업데이트
      }
    };

    fetchData();
  }, []);

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

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

  return (
    <div>
      {data.length > 0 ? (
        <>
          {data.map((data) => (
            <div key={data.id}>{data.title}</div>
          ))}
        </>
      ) : (
        <div>No Data Available</div>
      )}
    </div>
  );
};

export default MainPage;


작성한 함수가 아닌 곳에서 데이터를 필요로 할 경우

useEffect(() => {
  async function fetchData() {
    const res = await axios.get("API URL");
    const result = res.data;
    /* 1. 데이터를 return 하고 */
    return result;
  }
  /* 2. then을 통해 response를 넘긴다. */
  fetchData().then((res) => setDocs(res));
}, []);



6. .env로 환경 변수 관리하기

.env 파일을 사용하여 axios의 기본 URL 또는 기타 설정을 환경 변수로 관리할 수 있다.


.env 파일에 환경 변수를 정의하기

REACT_APP_SERVER_URL=http://localhost:3001


② 이제 프로젝트 어디에서든 process.env 객체를 통해 환경 변수에 접근할 수 있다.

⚠️ 환경 변수를 변경한 후에는 개발 서버를 재시작해야 변경 사항이 적용된다.

await axios.get("http://localhost:3001/todos"); // 변경 전
await axios.get(`${process.env.REACT_APP_SERVER_URL}/todos`); // 변경 후


만약 axios 인스턴스를 설정했다면, axios 인스턴스를 import하여 API 요청을 보내면 된다.

① 환경 변수로부터 기본 URL을 가져와서 axios.create를 사용하여 인스턴스를 설정하기

import axios from "axios";

// 환경 변수에서 기본 URL을 가져오기!
const apiUrl = process.env.REACT_APP_API_URL;

// axios 인스턴스 생성
const instance = axios.create({
  baseURL: apiUrl, // 기본 URL 설정
  // 추가 설정 가능 (예: headers, timeout 등)
});

export default instance;


② 설정한 axios 인스턴스를 import하여 API 요청을 보내면 된다.

import { useEffect, useState } from "react";
import api from "./axios/api"; // 설정한 axios 인스턴스 import

function MyComponent() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    api
      .get("/endpoint") // 기본 URL과 결합되어 요청이 된다.
      .then((response) => {
        setData(response.data);
      })
      .catch((err) => {
        setError(err);
      });
  }, []);

  if (error) return <div>Error: {error.message}</div>;
  if (!data) return <div>Loading...</div>;

  return (
    <div>
      <h1>Data:</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

export default MyComponent;



참조


카테고리:

업데이트:

댓글남기기