이 글은 Code with Mosh의 [Mastering React](https://codewithmosh.com/courses/enrolled/357787 과정을 공부하며 정리한 글입니다. 리알못이라 이상하거나 틀린 내용이 있을 것입니다…
React는 백엔드에 서비스 요청 시 개발자가 선호하는 라이브러리를 선택하여 사용하면 됩니다. 여기서는 Axios를 사용하여 백엔드에서 데이터를 가져오는 방법을 알아보겠습니다.
JSON Placeholder
JSON Placeholder는 Fake REST API를 제공하는 사이트입니다. 포스트, 이미지, 유저정보 등 다양한 가짜 API를 제공하여 백엔드 테스트용으로 사용하기게 유용한 곳입니다. 주소는 http://jsonplaceholder.typicode.com 입니다.
Axios 설치
리액트 프로젝트가 위치한 경로에서 npm install axios
명령어를 실행합니다.
데이터 가져오기
함수형 컴포넌트에서 사용하는 React Hooks 중 하나인 useEffect를 사용하여 컴포넌트가 렌더링될 때 데이터를 가져오도록 합니다. (useEffect에 대한 자세한 설명은 https://darrengwon.tistory.com/275를 참고하시기 바랍니다.) 클래스형 컴포넌트에서는 componentDidMounted() 함수에 렌더링될 때 데이터를 가져오도록 코딩하면 됩니다.
그리고 axios의 get() 함수를 사용하여 데이터를 가져옵니다.
import React, { useState, useEffect } from "react";
import axios from "axios";
import "./App.css";
const App = () => {
const [posts, setPosts] = useState([]);
const apiEndpoint = "http://jsonplaceholder.typicode.com/posts";
useEffect(async () => {
// Promise pending -> resolved(성공) 또눈 rejected(실패) 객체 반환
const response = await axios.get(apiEndpoint);
setPosts(response.data);
console.log(response.data);
}, []); // useEffect() 함수의 두번째 매개변수에 빈 배열을 넣어주면 컴포넌트가 렌더링 될 때만(한 번만) 실행됨
// ... 생략...
}
데이터 생성, 전송 및 추가
axios의 post() 함수를 사용하여 데이터를 백엔드로 보냅니다.
const App = () => {
const [posts, setPosts] = useState([]);
const apiEndpoint = "http://jsonplaceholder.typicode.com/posts";
const handleAdd = async () => {
// 백엔드로 보낼 데이터 생성
const obj = { title: 'a', body: 'b' };
// 백엔드로 데이터 전송. 이 때 새롭게 추가한 데이터에 대한 응답이 돌아옴
const response = await axios.post(apiEndpoint, obj);
console.log(response.data);
// state에 데이터 추가
const newPosts = [ data, ...posts ];
setPosts(newPosts);
};
// 생략...
}
데이터 수정
axios의 put() 또는 patch() 함수를 사용하여 수정할 데이터를 백엔드로 보냅니다. put() 함수는 하나 또는 그 이상의 속성을 수정할 때, patch() 함수는 모든 속성을 수정할 때 사용합니다.
const App = () => {
const [posts, setPosts] = useState([]);
const apiEndpoint = "http://jsonplaceholder.typicode.com/posts";
const handleUpdate = async (post) => {
// 수정할 속성
post.title = "UPDATED";
// 백엔드로 수정 데이터 전송. 이 때 수정된 데이터에 대한 응답이 돌아옴.
const response = await axios.put(`${apiEndpoint}/${post.id}`, post);
console.log(response.data);
// state 수정
const newPosts = [ ...posts ];
const index = posts.indexOf(post);
newPosts[index] = {...post};
setPosts(newPosts);
};
// 생략...
}
데이터 삭제
axios의 delete() 함수를 사용하여 삭제할 데이터를 백엔드로 보냅니다.
const App = () => {
const [posts, setPosts] = useState([]);
const apiEndpoint = "http://jsonplaceholder.typicode.com/posts";
const handleDelete = async (post) => {
// 백엔드로 삭제할 데이터 전송
await axios.delete(`${apiEndpoint}/${post.id}`);
// state 수정
const newPosts = posts.filter(p => p.id !== post.id);
setPosts(newPosts);
};
낙관적인(Optimistic) vs 비관적인(Pessimistic) 업데이트
위에서 설명한 CRUD 코드는 UI가 변경되기까지 약 0.5초 ~ 1초 정도가 소요됩니다. 그 이유는 먼저 백엔드와 통신하여 응답이 온 후 DOM에 변경사항을 반영하기 때문입니다. 이 방법은 백엔드와 먼저 통신한 후 성공하면 UI를 변경하지만 실패하면 UI를 변경하지 못합니다. 이 방법을 비관적(Pessimistic) 업데이트라고 합니다.
반면 백엔드와의 통신이 성공할지 또는 실패할지 확실하지 않을 때 구현하는 것을 낙관적(Optimistic) 업데이트라고 합니다. 먼저 UI를 변경한 후 백엔드와 통신을 한 뒤 통신이 실패하면 UI를 다시 원래대로 복구하는 것이 반응성 면에서 더 좋을 수 있습니다.
아래 코드는 낙관적(Optimistic) 업데이트 예제 코드입니다. try, catch문을 사용하여 오류를 검사한 뒤 오류가 발생하면 본래 값으로 원복하는 코드입니다.
const handleDelete = async (post) => {
// 백엔드 처리 실패 시 원복하기 위한 state
const originalPosts = posts;
// state 수정
const newPosts = posts.filter(p => p.id !== post.id);
setPosts(newPosts);
// 백엔드로 삭제할 데이터 전송
try {
await axios.delete(`${apiEndpoint}/${post.id}`);
// throw new Error(''); // 테스트용 코드
} catch (ex) {
alert('삭제 실패...');
setPosts(originalPosts);
}
};
예상되는(Expected) vs 예상치못한(Unexpected) 에러
예상되는 에러는 에러 발생 시 코드를 반환하는 에러입니다. 백엔드와 통신 시 404 Not Found, 400 Bad Request 에러 등이 그 예입니다.
예상치 못한 에러는 일반적인 상황에서는 발생할 수 없는 에러입니다. 네트워크나 서버가 다운되는 경우가 그 예입니다. 이런 에러들은 로그를 남기거나 에러메시지를 출력하여 예상치 못한 에러가 발생했음을 알려야 합니다.
// 백엔드로 삭제할 데이터 전송
try {
await axios.delete(`${apiEndpoint}/${post.id}`);
} catch (ex) {
// 예상되는 에러 처리
if (ex.response && ex.response.status === 404)
alert("포스트가 이미 삭제되었습니다.");
// 예상치 못한 에러 처리
else {
console.log("Logging the error:", ex);
alert("예상치 못한 에러가 발생했습니다.")
}
setPosts(originalPosts);
}
Axios Interceptors로 오류 통합 처리
Axios Interceptors를 사용하면 요청 또는 응답 오류를 통합하여 처리할 수 있습니다.
클래스형 컴포넌트 선언부 위에 axios.interceptors.response.use() 또는 axios.interceptors.request.use() 메소드를 구현해줍니다. 여기서는 response 쪽만 구현해봅니다.
// 응답에 대한 통합 처리
// 첫번째 매개변수: 성공했을 경우 실행할 콜백함수
// 두번째 매개변수: 실패했을 경우 실행할 콜백함수
axios.interceptors.response.use(null, error => {
const expectedError =
error.response &&
error.response.status <= 400 &&
error.response.status < 500;
if(!expectedError){
console.log('Axios Interceptors is Logging the error:', error);
alert("예상치못한 에러가 발생했습니다.");
}
return Promise.reject(error);
});
전체 코드 정리
import React, { useState, useEffect } from "react";
import axios from "axios";
import "./App.css";
const App = () => {
const [posts, setPosts] = useState([]);
const apiEndpoint = "http://jsonplaceholder.typicode.com/posts";
// axios의 interceptors를 사용하여 응답에 대한 에러 통합 처리
axios.interceptors.response.use(null, error => {
const expectedError =
error.response &&
error.response.status <= 400 &&
error.response.status < 500;
if(!expectedError)
console.log('Axios Interceptors is Logging the error:', error);
return Promise.reject(error);
});
// 컴포넌트 렌더링 처리: useEffect의 두번째 매개변수에 빈 배열 지정
useEffect(async () => {
const response = await axios.get(apiEndpoint);
setPosts(response.data);
console.log("useEffect():", response.data);
}, []);
// 백엔드로 데이터 전송
const handleAdd = async () => {
const obj = { title: 'a', body: 'b' };
const response = await axios.post(apiEndpoint, obj);
console.log("handleAdd():", response.data);
const newPosts = [ response.data, ...posts ];
setPosts(newPosts);
};
// 백엔드로 수정 데이터 전송
const handleUpdate = async (post) => {
post.title = "UPDATED";
const response = await axios.put(`${apiEndpoint}/${post.id}`, post);
console.log("handleUpdate():", response.data);
const newPosts = [ ...posts ];
const index = posts.indexOf(post);
newPosts[index] = {...post};
setPosts(newPosts);
};
// 백엔드로 삭제할 데이터 전송
const handleDelete = async (post) => {
const originalPosts = posts;
const newPosts = posts.filter(p => p.id !== post.id);
setPosts(newPosts);
try {
const response = await axios.delete(`${apiEndpoint}/${post.id}`);
console.log("handleDelete()", response);
}
catch (ex) {
if(ex.response && ex.response.status === 404)
alert("포스트가 이미 삭제되었습니다.");
setPosts(originalPosts);
}
};
return (
<React.Fragment>
<button className="btn btn-primary" onClick={handleAdd}>
Add
</button>
<table className="table">
<thead>
<tr>
<th>Title</th>
<th>Update</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<tr key={post.id}>
<td>{post.title}</td>
<td>
<button
className="btn btn-info btn-sm"
onClick={() => handleUpdate(post)}
>
Update
</button>
</td>
<td>
<button
className="btn btn-danger btn-sm"
onClick={() => handleDelete(post)}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</React.Fragment>
);
};
export default App;