React 비동기 데이터 처리

cloud2000·2025년 11월 13일

Fetch 사용

  • 기존 사용하는 XMLHttpRequest를 대체함.
  • 첫번째 인수로 요청 URL을, 두번째 인수로 옵션 객체(method, headers, body 등의 속성)를 입력받음

Todo 예

"use client";

import { useState } from "react";

type Todos = {
	userId: string;
	id: number;
	title: string;
	completed: boolean;
};

const TodosFetchPage = () => {
	const [todoId, setTodoId] = useState("");
	const [todos, setTodos] = useState<Todos[]>([]);

	const getTodos = async () => {
		try {
			const response = await fetch(
				"https://jsonplaceholder.typicode.com/todos"
			);

			if (!response.ok) {
				throw new Error("Network response was not ok");
			}

			const data = await response.json();
			setTodos(data);
		} catch (error) {
			console.error(error);
		}
	};

	const postTodo = async () => {
		try {
			const response = await fetch(
				"https://jsonplaceholder.typicode.com/todos",
				{
					method: "POST",
					headers: {
						"Content-Type": "application/json",
					},
					body: JSON.stringify({
						userId: "1000",
						title: "New Title",
						completed: false,
					}),
				}
			);

			if (!response.ok) {
				throw new Error("Network response was not ok");
			}

			const data = await response.json();
			setTodos([...todos, data]);
		} catch (error) {
			console.error(error);
		}
	};

	const toggleTodo = async () => {
		try {
			const response = await fetch(
				`https://jsonplaceholder.typicode.com/todos/${todoId}`,
				{
					method: "PATCH",
					body: JSON.stringify({ completed: true }),
				}
			);

			if (!response.ok) {
				throw new Error("Network response was not ok");
			}

			const data = await response.json();

			setTodos(
				todos.map((todo) =>
					todo.id === Number(todoId) ? { ...todo, done: data.done } : todo
				)
			);
		} catch (error) {
			console.error(error);
		}
	};

	const updateTodo = async () => {
		try {
			const response = await fetch(
				`https://jsonplaceholder.typicode.com/todos/${todoId}`,
				{
					method: "PUT",
					headers: {
						"Content-Type": "application/json",
					},
					body: JSON.stringify({ title: "Update Todo" }),
				}
			);

			if (!response.ok) {
				console.log(response);
				throw new Error("Network response was not ok");
			}

			const data = await response.json();

			setTodos(todos.map((todo) => (todo.id === Number(todoId) ? data : todo)));
		} catch (error) {
			console.error(error);
		}
	};

	const deleteTodo = async () => {
		try {
			const response = await fetch(
				`https://jsonplaceholder.typicode.com/todos/${todoId}`,
				{
					method: "DELETE",
				}
			);

			if (!response.ok) {
				throw new Error("Network response was not ok");
			}

			setTodos(todos.filter((todo) => todo.id !== Number(todoId)));
		} catch (error) {
			console.error(error);
		}
	};

	return (
		<div>
			<h1>Todos</h1>
			<pre>{JSON.stringify(todos, null, 2)}</pre>

			<div>
				<label htmlFor="todoId"> Todo ID: </label>
				<input
					id="todoId"
					className="border p-3 rounded mb"
					type="text"
					value={todoId}
					onChange={(e) => setTodoId(e.target.value)}
				/>
			</div>
			<button
				className="mr-3 bg-blue-700 text-white py-3 rounded hover:bg-blue-800"
				onClick={getTodos}
			>
				Get Todos
			</button>
			<button
				className="mr-3 bg-blue-700 text-white py-3 rounded hover:bg-blue-800"
				onClick={postTodo}
			>
				Post Todos
			</button>
			<button
				className="mr-3 bg-blue-700 text-white py-3 rounded hover:bg-blue-800"
				onClick={toggleTodo}
			>
				Toggle Todos
			</button>
			<button
				className="mr-3 bg-blue-700 text-white py-3 rounded hover:bg-blue-800"
				onClick={updateTodo}
			>
				Update Todos
			</button>
			<button
				className="mr-3 mb-3  bg-blue-700 text-white py-3 rounded hover:bg-blue-800"
				onClick={deleteTodo}
			>
				Delete Todos
			</button>
		</div>
	);
};

export default TodosFetchPage;

Axios 사용

  • Axios는 웹브라우저에서 HTTP 통신을 쉽게 처리하기 위한 라이브러리임.
  • Fetch API 보다 더 다양한 기능을 제공함.
  • config는 method, url, data, headers를 설정할 수 있다.
  • axios()함수는 response.data로 바로 응답 객체를 접근할 수 있다. fetch()는 reponse.json()을 따로 호출해야 한다.
  • axios()는 응답이 실패한 경우 자동으로 catch 블록으로 오류를 반환한다. fetch()처럼 일일이 response.ok를 체크할 필요가 없다.
npm install axios
// Promise 기반 then() 방식
axios(config)
.then(response => console.log(response))
.catch(error => console.error('Error:', error));

// async/await 방식
const fetchData = async () => {
  try{
    const response = await axios(config);
    console.log(response.data)
  } catch (error) {
    console.log(error)
  }
  
}

Todo 예

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

type Todos = {
	userId: string;
	id: number;
	title: string;
	completed: boolean;
};

const TodosPage = () => {
	const [todos, setTodos] = useState<Todos[]>([]);

	const getTodos = async () => {
		try {
			const response = await axios.get(
				"https://jsonplaceholder.typicode.com/todos"
			);
			setTodos(response.data);
		} catch (error) {
			console.error(error);
		}
	};

	const postTodo = async () => {
		try {
			const response = await axios.post(
				"https://jsonplaceholder.typicode.com/todos",
				{
					title: "New Title",
				}
			);
			setTodos([...todos, response.data]);
		} catch (error) {
			console.error(error);
		}
	};

	const toggleTodo = async () => {
		const todoId = 1;
		try {
			const response = await axios.patch(
				`https://jsonplaceholder.typicode.com/todos/${todoId}/done`
			);
			setTodos(
				todos.map((todo) =>
					todo.id === Number(todoId)
						? { ...todo, done: response.data.done }
						: todo
				)
			);
		} catch (error) {
			console.error(error);
		}
	};

	const updateTodo = async () => {
		const todoId = 1;
		try {
			const response = await axios.put(
				`https://jsonplaceholder.typicode.com/todos/${todoId}`,
				{
					title: "Updated toto",
				}
			);
			setTodos(
				todos.map((todo) => (todo.id === Number(todoId) ? response.data : todo))
			);
		} catch (error) {
			console.error(error);
		}
	};

	const deleteTodo = async () => {
		const todoId = 1;
		try {
			await axios.delete(
				`https://jsonplaceholder.typicode.com/todos/${todoId}`
			);
			setTodos(todos.filter((todo) => todo.id !== Number(todoId)));
		} catch (error) {
			console.error(error);
		}
	};
};

export default TodosPage;

Tips

useState(), useEffect()를 이용하여 화면 출력 전 API 호출하기

import { useState, useEffect } from "react";

const App = () => {
	// 초기값인 빈배열([])을 보고 never 또는 any type으로 추정
	// 이에 따라 아래 map()으로 순회할 경우 type error(never' 형식에 'title' 속성이 없습니다) 발생
	// const [data, setData] = useState([])

	// 아래와 같이 객체로 type을 명시하면 해결됨.
	const [data, setData] = useState<
		{ id: number; title: string; completed: boolean }[]
	>([]);
	useEffect(() => {
		const fetchData = async () => {
			const response = await fetch(
				"https://jsonplaceholder.typicode.com/todos"
			);
			if (!response.ok) {
				throw new Error("데이터를 불러오지 못했습니다.");
			}
			const data = await response.json();
			setData(data);
		};

		fetchData();
	}, []);

	return (
		<div>
			{/* <pre>{JSON.stringify(data, null, 2)}</pre> */}

			{/* never' 형식에 'title' 속성이 없습니다 */}
			{data.map((todo) => (
				<li key={todo.id}>{todo.title}</li>
			))}
		</div>
	);
};

오류 처리

import { useState, useEffect } from "react";

const App = () => {
	const [data, setData] = useState<
		{ id: number; title: string; completed: boolean }[]
	>([]);

	const [error, setError] = useState("");
	useEffect(() => {
		const fetchData = async () => {
			try {
				const response = await fetch(
					"https://jsonplaceholder.typicode.com/todos"
				);
				if (!response.ok) {
					throw new Error("데이터를 불러오지 못했습니다.");
				}
				const data = await response.json();
				setData(data);
			} catch (error) {
				setError(
                	// catch의 error는 자동으로 unknown으로 추론됨으로 Error instance인지 확인한후 안전하게 .message 속성에 접근하도록 함.
					error instanceof Error
						? error.message
						: "알 수 없는 오류가 발생했습니다."
				);
			}
		};

		fetchData();
	}, []);
    
	if(error) return <div>{error}</div>    

	return (
		<div>
			{data.map((todo) => (
				<li key={todo.id}>{todo.title}</li>
			))}
		</div>
	);
};

Loading 중 표시

import { useState, useEffect } from "react";

const App = () => {
	const [data, setData] = useState<
		{ id: number; title: string; completed: boolean }[]
	>([]);
	const [error, setError] = useState("");
	const [isLoading, setIsLoading] = useState(true);

	useEffect(() => {
		const fetchData = async () => {
			try {
				const response = await fetch(
					"https://jsonplaceholder.typicode.com/todos"
				);
				if (!response.ok) {
					throw new Error("데이터를 불러오지 못했습니다.");
				}
				const data = await response.json();
				setData(data);
			} catch (error) {
				setError(
					error instanceof Error
						? error.message
						: "알 수 없는 오류가 발생했습니다."
				);
			} finally {
				setIsLoading(false);
			}
		};

		fetchData();
	}, []);

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

	return (
		<div>
			{data.map((todo) => (
				<li key={todo.id}>{todo.title}</li>
			))}
		</div>
	);
};

데이터 요청 취소하기

import { useState, useEffect } from "react";

const App = () => {
	const [data, setData] = useState<
		{ id: number; title: string; completed: boolean }[]
	>([]);
	const [error, setError] = useState("");
	const [isLoading, setIsLoading] = useState(true);

	useEffect(() => {
		const controller = new AbortController();
		const signal = controller.signal;

		const fetchData = async () => {
			try {
				const response = await fetch(
					"https://jsonplaceholder.typicode.com/todos",
					{ signal }
				);
				if (!response.ok) {
					throw new Error("데이터를 불러오지 못했습니다.");
				}
				const data = await response.json();
				setData(data);
				setIsLoading(false);
			} catch (error) {
				setError(
					error instanceof Error
						? error.message
						: "알 수 없는 오류가 발생했습니다."
				);
				setIsLoading(false);
			} finally {
				setIsLoading(false);
			}
		};

		fetchData();
		return () => {
			controller.abort();
		};
	}, []);

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

	return (
		<div>
			{data.map((todo) => (
				<li key={todo.id}>{todo.title}</li>
			))}
		</div>
	);
};
profile
클라우드쟁이

0개의 댓글