리알못 React: 7. 백엔드(Backend)

Table of Content

이 글은 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;

댓글 남기기