[React] 비동기 프로그래밍 정리⭐
비동기를 다시 공부하다보니 fetch, axios, promise, async와 await, then~catch, try~catch와 같은 용어들이 혼동이 와서 이를 정리하는 글이다.

추천 공부 순서🔗
- 비동기 프로그래밍 정리
- 프로미스 객체와 메서드
- async와 await
- json-server
- fetch()함수로 HTTP 요청하기
- JSON과 비동기통신
- 비동기 통신 - axios와 interceptor
- React 비동기 데이터 처리 최적화: 로딩, 에러, 재시도 로직
- 비동기 프로그래밍과 REST API 호출 연습
- 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)이다.
-
콜백지옥과 프로미스 체이닝 단점을 해결해준다.
- 프로미스를 간결하게 만들어 줌
- 에러 핸들링에 유리(try…catch 구문을 사용)
- 에러 위치를 찾기 쉬움
- 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 - 네트워크 요청/응답 처리
fetch와axios는 프로미스(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));
}, []);
fetchAPI로 데이터 가져오기.then()과.catch()를 사용한 프로미스 처리
하지만, fetch는 아래와 같은 문제가 있다.
- ⭐예외 처리의 문제: 네트워크 오류 시에만 예외 발생시키고 HTTP 오류에 대해선 예외 발생시키지 않는다.
- 프로미스 체이닝의 가독성 문제:
.then()과.catch()로 구성된 프로미스 체이닝이 길어지면 코드의 가독성이 떨어짐 -
어떤 HTTP 메서드를 사용하는지 불명확
- 자동 JSON 변환 없음
관련 포스팅
3.2 axios - fetch 단점 대안
axios는 HTTP 요청을 보내기 위해 사용하는 외부 라이브러리로, npm 또는 yarn을 통해 설치해야 한다.
- fetch보다 간편하게 사용할 수 있으며, 다양한 기능을 제공한다.
- 자동 josn 변환
- 에러처리
- 요청 취소
- 요청에 대한 타임아웃 설정
- 인터셉터(intercepter)
- aixos 요청 및 응답을 가로채어 변형하는 역할을 한다.
- 보통 axios 인스턴스를 생성한 후, 하위에 인터셉터를 사용한다.
- 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로부터 받아온 데이터를 렌더링하기 위해, 주로
useState와useEffect훅을 사용하여 데이터를 가져오고 상태에 저장한 다음, 데이터를 컴포넌트에 렌더링한다.
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;
댓글남기기