React 컴포넌트에서 API 연동하기

2025. 4. 1. 00:55Frameworks/React

React 컴포넌트에서 API 연동하기

1. useState와 useEffect를 사용한 기본 연동

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function PostList() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 컴포넌트 마운트 시 데이터 가져오기
    const fetchPosts = async () => {
      try {
        setLoading(true);
        const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
        setPosts(response.data);
        setError(null);
      } catch (err) {
        setError('데이터를 불러오는데 실패했습니다.');
        setPosts([]);
      } finally {
        setLoading(false);
      }
    };

    fetchPosts();
  }, []); // 빈 의존성 배열은 컴포넌트 마운트 시 한 번만 실행

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

  return (
    <div>
      <h1>포스트 목록</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default PostList;

2. 사용자 입력에 따른 API 요청

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function UserSearch() {
  const [query, setQuery] = useState('');
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // 쿼리가 변경될 때마다 API 요청
  useEffect(() => {
    // 쿼리가 비어있으면 API 호출 안 함
    if (!query) {
      setUsers([]);
      return;
    }

    // 디바운스 처리 (타이핑 중 연속적인 API 호출 방지)
    const timer = setTimeout(() => {
      const fetchUsers = async () => {
        try {
          setLoading(true);
          const response = await axios.get(
            `https://api.github.com/search/users?q=${query}`
          );
          setUsers(response.data.items);
          setError(null);
        } catch (err) {
          setError('사용자를 검색하는데 실패했습니다.');
          setUsers([]);
        } finally {
          setLoading(false);
        }
      };

      fetchUsers();
    }, 500); // 500ms 지연

    // 클린업 함수: 타이머 취소
    return () => clearTimeout(timer);
  }, [query]);

  return (
    <div>
      <h1>GitHub 사용자 검색</h1>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="사용자 이름 입력"
      />

      {loading && <div>검색 중...</div>}
      {error && <div>에러: {error}</div>}

      <ul>
        {users.map(user => (
          <li key={user.id}>
            <img src={user.avatar_url} alt={user.login} width="50" />
            <a href={user.html_url} target="_blank" rel="noopener noreferrer">
              {user.login}
            </a>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserSearch;
 

3. CRUD 작업 구현

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [newTodoText, setNewTodoText] = useState('');

  // 할 일 목록 가져오기
  const fetchTodos = async () => {
    try {
      setLoading(true);
      const response = await axios.get('https://jsonplaceholder.typicode.com/todos?_limit=10');
      setTodos(response.data);
      setError(null);
    } catch (err) {
      setError('할 일 목록을 불러오는데 실패했습니다.');
    } finally {
      setLoading(false);
    }
  };

  // 컴포넌트 마운트 시 할 일 목록 가져오기
  useEffect(() => {
    fetchTodos();
  }, []);

  // 새 할 일 추가
  const addTodo = async (e) => {
    e.preventDefault();
    if (!newTodoText.trim()) return;

    try {
      const response = await axios.post('https://jsonplaceholder.typicode.com/todos', {
        title: newTodoText,
        completed: false,
        userId: 1, // 임의의 사용자 ID
      });

      // 응답으로 받은 새 할 일을 목록에 추가 (ID가 할당된 데이터)
      setTodos([...todos, response.data]);
      setNewTodoText('');
    } catch (err) {
      setError('할 일을 추가하는데 실패했습니다.');
    }
  };

  // 할 일 완료 상태 토글
  const toggleTodo = async (id) => {
    try {
      const todoToUpdate = todos.find(todo => todo.id === id);
      const updatedTodo = { ...todoToUpdate, completed: !todoToUpdate.completed };

      // API 요청으로 상태 업데이트
      await axios.put(`https://jsonplaceholder.typicode.com/todos/${id}`, updatedTodo);

      // 로컬 상태 업데이트
      setTodos(todos.map(todo => 
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      ));
    } catch (err) {
      setError('할 일 상태를 변경하는데 실패했습니다.');
    }
  };

  // 할 일 삭제
  const deleteTodo = async (id) => {
    try {
      // API 요청으로 삭제
      await axios.delete(`https://jsonplaceholder.typicode.com/todos/${id}`);

      // 로컬 상태에서 삭제된 할 일 제거
      setTodos(todos.filter(todo => todo.id !== id));
    } catch (err) {
      setError('할 일을 삭제하는데 실패했습니다.');
    }
  };

  if (loading && todos.length === 0) return <div>로딩 중...</div>;

  return (
    <div>
      <h1>할 일 목록</h1>
      
      {error && <div style={{ color: 'red' }}>{error}</div>}

      <form onSubmit={addTodo}>
        <input
          type="text"
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
          placeholder="새 할 일 입력"
        />
        <button type="submit">추가</button>
      </form>

      <ul>
        {todos.map(todo => (
          <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            {todo.title}
            <button onClick={() => deleteTodo(todo.id)}>삭제</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoApp;

커스텀 훅으로 API 로직 추상화

API 호출 로직을 재사용 가능한 커스텀 훅으로 분리하면 코드 중복을 줄이고 관심사를 분리할 수 있습니다.

기본 데이터 가져오기 훅

// hooks/useFetch.js
import { useState, useEffect } from 'react';
import axios from 'axios';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await axios.get(url);
        setData(response.data);
        setError(null);
      } catch (err) {
        setError('데이터를 불러오는데 실패했습니다.');
        setData(null);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

export default useFetch;

훅 사용 예:

import React from 'react';
import useFetch from '../hooks/useFetch';

function PostList() {
  const { data: posts, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');

  if (loading) return <div>로딩 중...</div>;
  if (error) return <div>에러: {error}</div>;
  if (!posts) return <div>데이터가 없습니다.</div>;

  return (
    <div>
      <h1>포스트 목록</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default PostList;

 

더 복잡한 API 훅 (CRUD 기능 포함)

// hooks/useAPI.js
import { useState } from 'react';
import axios from 'axios';

function useAPI(baseURL) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // GET 요청
  const get = async (endpoint = '') => {
    try {
      setLoading(true);
      const response = await axios.get(`${baseURL}${endpoint}`);
      setData(response.data);
      setError(null);
      return response.data;
    } catch (err) {
      setError('데이터를 불러오는데 실패했습니다.');
      throw err;
    } finally {
      setLoading(false);
    }
  };

  // POST 요청
  const post = async (endpoint = '', payload) => {
    try {
      setLoading(true);
      const response = await axios.post(`${baseURL}${endpoint}`, payload);
      setError(null);
      return response.data;
    } catch (err) {
      setError('데이터를 생성하는데 실패했습니다.');
      throw err;
    } finally {
      setLoading(false);
    }
  };

  // PUT 요청
  const put = async (endpoint = '', payload) => {
    try {
      setLoading(true);
      const response = await axios.put(`${baseURL}${endpoint}`, payload);
      setError(null);
      return response.data;
    } catch (err) {
      setError('데이터를 업데이트하는데 실패했습니다.');
      throw err;
    } finally {
      setLoading(false);
    }
  };

  // DELETE 요청
  const del = async (endpoint = '') => {
    try {
      setLoading(true);
      const response = await axios.delete(`${baseURL}${endpoint}`);
      setError(null);
      return response.data;
    } catch (err) {
      setError('데이터를 삭제하는데 실패했습니다.');
      throw err;
    } finally {
      setLoading(false);
    }
  };

  return { data, loading, error, get, post, put, del };
}

export default useAPI;

훅 사용 예:

import React, { useState, useEffect } from 'react';
import useAPI from '../hooks/useAPI';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [newTodoText, setNewTodoText] = useState('');
  const api = useAPI('https://jsonplaceholder.typicode.com');

  // 할 일 목록 가져오기
  useEffect(() => {
    const fetchTodos = async () => {
      try {
        const data = await api.get('/todos?_limit=10');
        setTodos(data);
      } catch (error) {
        console.error('할 일 목록을 불러오는데 실패했습니다:', error);
      }
    };

    fetchTodos();
  }, []);

  // 새 할 일 추가
  const addTodo = async (e) => {
    e.preventDefault();
    if (!newTodoText.trim()) return;

    try {
      const newTodo = await api.post('/todos', {
        title: newTodoText,
        completed: false,
        userId: 1
      });
      setTodos([...todos, newTodo]);
      setNewTodoText('');
    } catch (error) {
      console.error('할 일을 추가하는데 실패했습니다:', error);
    }
  };

  // 할 일 완료 상태 토글
  const toggleTodo = async (id) => {
    try {
      const todoToUpdate = todos.find(todo => todo.id === id);
      const updatedTodo = { ...todoToUpdate, completed: !todoToUpdate.completed };
      
      await api.put(`/todos/${id}`, updatedTodo);
      setTodos(todos.map(todo => 
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      ));
    } catch (error) {
      console.error('할 일 상태를 변경하는데 실패했습니다:', error);
    }
  };

  // 할 일 삭제
  const deleteTodo = async (id) => {
    try {
      await api.del(`/todos/${id}`);
      setTodos(todos.filter(todo => todo.id !== id));
    } catch (error) {
      console.error('할 일을 삭제하는데 실패했습니다:', error);
    }
  };

  return (
    <div>
      <h1>할 일 목록</h1>
      
      {api.error && <div style={{ color: 'red' }}>{api.error}</div>}
      {api.loading && <div>로딩 중...</div>}

      <form onSubmit={addTodo}>
        <input
          type="text"
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
          placeholder="새 할 일 입력"
        />
        <button type="submit" disabled={api.loading}>추가</button>
      </form>

      <ul>
        {todos.map(todo => (
          <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
              disabled={api.loading}
            />
            {todo.title}
            <button onClick={() => deleteTodo(todo.id)} disabled={api.loading}>삭제</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoApp;
 

'Frameworks > React' 카테고리의 다른 글

에러 처리와 로딩 상태 관리  (0) 2025.04.01
고급 데이터 가져오기 라이브러리  (0) 2025.04.01
React와 API 연동하기  (0) 2025.04.01
Context API vs Redux  (0) 2025.04.01
Redux Toolkit  (0) 2025.04.01