📌 데이터 패칭(Data Fetching)
: 외부 API나 데이터베이스 등에서 데이터를 가져오는(읽는) 작업
- 서버에 데이터 요청/응답 (읽기(Read))
- 비동기적
: 네트워크 요청은 서버에서 데이터를 가져오는 동안 비동기적으로 처리됨 - 주로 GET 요청
: 데이터 패칭은 HTTP `GET` 요청을 통해 서버에서 데이터를 조회하는 형태 - API 상태 관리
: 데이터 패칭 과정에서 `isLoading, data, error` 등 API 상태를 관리해야 함 - 서버의 데이터를 가져오는 작업은 이펙트(Effect)를 사용한다.
✅ 이펙트(Effect) 사용
- API 서버에 데이터를 요청해 응답받은 데이터를 렌더링
- 이펙트를 사용해 Promise 또는 Async / await를 사용해 데이터 가져오기를 요청
- 데이터 가져오기 요청 응답이 성공인 경우, 리액트 앱에 데이터를 렌더링
- 데이터 가져오기 요청 응답에 문제가 발생한 경우, 리액트 앱에 오류 메시지를 렌더링
- 관련없는 패치가 앱에 영향을 주지않도록 클린업 함수에서 무시하도록 설정
- AbortController를 사용해 중복된 네트워크 요청을 중단
1. API 주소 정의
- API 요청을 보낼 URL 정의
- 이 주소로 데이터 요청
// API 주소 정의
const ENDPOINT = '//yamoo9.pockethost.io/api/collections/olive_oil/records';
2. 상태 관리 객체 정의
- useImmer를 사용하여 상태 객체를 정의
- isLoading, error, data를 상태로 관리
const [state, setState] = useImmer({
isLoading: false, // 데이터가 로딩 중인지 저장
error: null, // 요청 중 발생한 오류를 저장
data: null, // API로부터 받아온 데이터를 저장
});
3. AbortController 생성
- AbortController를 생성하여 네트워크 요청을 취소할 수 있는 기능을 제공
const abortController = new AbortController();
4. 로딩 상태 설정
- setState를 사용하여 데이터 로딩 중임을 나타내는 isLoading 상태를 true로 설정
setState((draft) => {
draft.isLoading = true;
});
5. 데이터 패칭 함수 정의
- 비동기 함수를 정의하여 API로부터 데이터를 비동기로 가져옴
async function fetchOliveOil() {
try {
const response = await fetch(ENDPOINT, {
signal: abortController.signal,
});
6. API 요청
- fetch를 사용하여 API로 데이터를 요청
- signal을 통해 요청 취소 가능
const response = await fetch(ENDPOINT, {
signal: abortController.signal,
});
7. 응답 데이터 변환
- response.json()을 사용하여 응답 본문을 JSON 객체로 변환
const responseData = await response.json();
8. 응답 검증
- 응답이 정상적이지 않으면, 응답 데이터의 메시지를 포함한 오류를 발생
if (!response.ok) {
throw new Error(responseData.message);
}
9. 상태 업데이트
- 응답 데이터와 isLoading 상태를 업데이트하여 데이터가 성공적으로 로딩되었음을 나타냄
setState((draft) => {
draft.data = responseData;
draft.isLoading = false;
});
10. 오류 처리
- DOMException을 제외한 오류를 처리
- 오류 상태를 error로 업데이트
- isLoading을 false로 설정하여 로딩 상태 종료
catch (error) {
if (!(error instanceof DOMException)) {
// 오류 상태 업데이트
setState((draft) => {
draft.error = error;
draft.isLoading = false;
});
}
}
11. 데이터 패칭 호출
- fetchOliveOil 함수를 호출하여 데이터를 요청
fetchOliveOil();
12. 클린업 함수
- 컴포넌트가 언마운트될 때, abortController.abort()를 호출하여 네트워크 요청을 중단
return () => {
abortController.abort();
};
13. 의존성 배열
- useEffect의 의존성 배열에 setState를 추가하여 setState가 변경될 때마다 useEffect가 재실행되도록 함
}, [setState]);
14. 조건부 렌더링
- 데이터 로딩 중일 때 로딩 메시지를 렌더링
- 오류가 발생했을 때 오류 메시지를 렌더링
if (state.isLoading) {
return <LoadingMessage />;
}
if (state.error) {
return <PrintError error={state.error} />;
}
15. 로딩 메시지 컴포넌트
- 데이터 로딩 중일 때 표시되는 메시지를 렌더링
function LoadingMessage() {
return <p>데이터 로딩 중...</p>;
}
16. 오류 메시지 컴포넌트
- 오류가 발생했을 때 표시되는 오류 메시지를 렌더링
- propTypes로 타입을 검사
PrintError.propTypes = {
error: exact({
message: string.isRequired,
}).isRequired,
};
function PrintError({ error }) {
return (
<p role="alert">
오류 발생!{' '}
<span style={{ fontWeight: 500, color: 'red' }}>{error.message}</span>
</p>
);
}
17. 데이터 렌더링
- 데이터 항목을 리스트 형태로 렌더링
return (
<div className={S.component}>
<ul>
{state.data?.items.map?.((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
18. 로딩 메시지 컴포넌트 정의
- 데이터 로딩 중 메시지를 표시하는 컴포넌트를 정의
function LoadingMessage() {
return <p>데이터 로딩 중...</p>;
}
19. 오류 메시지 컴포넌트 정의
- 오류 메시지를 표시하는 컴포넌트를 정의
function PrintError({ error }) {
return (
<p role="alert">
오류 발생!{' '}
<span style={{ fontWeight: 500, color: 'red' }}>{error.message}</span>
</p>
);
}
20. 오류 메시지 컴포넌트와 타입 검사
- PrintError 컴포넌트의 error prop 타입을 검사
PrintError.propTypes = {
error: exact({
message: string.isRequired,
}).isRequired,
};
예제 1. 데이터 패칭
import { useEffect } from 'react';
import { useImmer } from 'use-immer';
import { exact, string } from 'prop-types';
import S from './DataFetching.module.css';
// 1. API 주소 정의
const ENDPOINT = '//yamoo9.pockethost.io/api/collections/olive_oil/records';
function DataFetching() {
// 2. API 요청의 상태를 관리하는 객체 정의
// - isLoading: 데이터가 로딩 중인지 여부
// - error: 요청 중 발생한 오류
// - data: API로부터 받아온 데이터
const [state, setState] = useImmer({
isLoading: false,
error: null,
data: null,
});
useEffect(() => {
// 3. AbortController 인스턴스 생성
// - 네트워크 요청을 취소할 수 있게 해줍니다.
const abortController = new AbortController();
// 4. isLoading 상태를 true로 설정하여 데이터 로딩 중임을 나타냄
setState((draft) => {
draft.isLoading = true;
});
// 5. 비동기로 데이터를 가져오는 함수 정의
async function fetchOliveOil() {
try {
// 6. API 엔드포인트로부터 데이터를 가져옴
// - AbortController의 signal을 설정하여 요청을 취소할 수 있도록 함
const response = await fetch(ENDPOINT, {
signal: abortController.signal,
});
// 7. 응답 본문을 JSON 객체로 변환
const responseData = await response.json();
// 8. 응답이 정상적이지 않으면 오류 발생
if (!response.ok) {
throw new Error(responseData.message);
}
// 9. 응답 데이터와 로딩 상태 업데이트
// - data 상태에 응답 데이터를 설정하고, isLoading을 false로 업데이트
setState((draft) => {
draft.data = responseData;
draft.isLoading = false;
});
} catch (error) {
// 10. AbortController의 signal로 인해 발생할 수 있는 DOMException을 제외한 나머지 오류 처리
if (!(error instanceof DOMException)) {
// 11. 오류 상태와 로딩 상태 업데이트
// - error 상태에 오류를 저장하고, isLoading을 false로 설정
setState((draft) => {
draft.error = error;
draft.isLoading = false;
});
}
}
}
// 12. 데이터 패칭 함수 호출
fetchOliveOil();
// 13. 컴포넌트 언마운트 시 fetch 요청을 중단하는 클린업 함수 반환
return () => {
abortController.abort();
};
}, [setState]); // 14. useEffect의 의존성 배열에 setState를 추가하여 상태 업데이트 함수가 변경될 때마다 useEffect가 재실행되도록 함
// 15. 조건부 렌더링
if (state.isLoading) {
// 16. 데이터가 로딩 중일 때 로딩 중 메시지 컴포넌트 렌더링
return <LoadingMessage />;
}
if (state.error) {
// 17. 오류가 발생했을 때 오류 메시지 컴포넌트 렌더링
return <PrintError error={state.error} />;
}
// 18. 데이터가 정상적으로 로딩되었을 때, 데이터 항목을 리스트로 렌더링
return (
<div className={S.component}>
<ul>
{state.data?.items.map?.((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
/* ----------------------------------------------- */
// 19. 로딩 중 메시지 컴포넌트 정의
function LoadingMessage() {
return <p>데이터 로딩 중...</p>;
}
// 20. 오류 메시지 컴포넌트와 타입 검사
PrintError.propTypes = {
error: exact({
message: string.isRequired,
}).isRequired,
};
// 21. 오류 메시지 컴포넌트 정의
function PrintError({ error }) {
return (
<p role="alert">
오류 발생!{' '}
<span style={{ fontWeight: 500, color: 'red' }}>{error.message}</span>
</p>
);
}
export default DataFetching;
📌 데이터 뮤테이션(Data Mutation)
: 서버의 데이터를 변경하는 작업
- 서버의 데이터 변경 요청/응답 (생성(Create), 업데이트(Update), 삭제(Delete))
- 비동기적
: 서버에 데이터를 보낸 후 결과를 기다리는 비동기 작업으로 처리됨 - 주로 POST / PUT / PATCH / DELETE 요청
- POST: 새로운 데이터를 생성
- PUT/PATCH: 기존 데이터를 업데이트
- DELETE: 데이터를 삭제
- 상태 관리
: 뮤테이션 중에도 `isMutating, success, error`와 같이 데이터 상태를 관리 - Optimistic Updates
: 네트워크 응답을 기다리기 전에 UI에서 먼저 상태를 업데이트하여 빠른 사용자 경험을 제공할 수도 있음 - 서버의 데이터를 추가, 수정, 삭제하는 것은 이벤트(events)로 처리한다.
✅ 이벤트(Event) 사용
- 비동기 함수 작성: async 함수로 CRUD 작업을 비동기적으로 처리할 수 있도록 함.
- 백엔드 API 엔드포인트 정의: 요청할 API 서버 주소를 설정.
- 서버에 요청 보내기: fetch API로 서버에 요청을 전송.
- 전송할 데이터 준비 (POST, PATCH 요청의 경우): 데이터를 JSON 포맷으로 변환하여 전송.
- 서버 응답 대기 및 처리: await 키워드로 서버의 응답을 대기한 후 처리.
- 에러 핸들링: 응답이 성공적이지 않으면 에러를 던져 처리.
- 응답 데이터 반환: 서버로부터 받아온 데이터를 클라이언트에 반환.
1. 비동기 함수 작성
- async 키워드를 사용하여 비동기 작업을 처리, Promise 객체를 반환
- 비동기 함수 내부에서 await 키워드를 사용하여 서버 요청에 대한 응답을 기다림
export async function createNote() { ... }
export async function readNotes() { ... }
export async function readNoteOne() { ... }
export async function updateNote() { ... }
export async function deleteNote() { ... }
2. 백엔드 API 엔드포인트
- 서버와 통신할 때 사용할 서버의 기본 주소를 API 엔드포인트(ENDPOINT)로 설정
- 각 함수에서 필요한 API URL을 조합하여 사용
const ENDPOINT = 'http://127.0.0.1:8090';
3. API URL을 저장 & 데이터 요청
- API 엔드포인트(ENDPOINT)를 조합하여 필요한 서버 API URL을 저장
const REQUEST_URL = `${ENDPOINT}/api/collections/notes/records`;
4. HTTP 요청 시 전송할 JSON 포맷 문자열
- 서버에 데이터를 전송할 때, JSON 형식으로 변환하여 전송
- JSON.stringify() / JSON.parse()를 사용하여 자바스크립트 객체(JSON) ↔️ 문자열 변환
const body = JSON.stringify({
title: newNote.title,
description: newNote.description,
});
5. 서버에서 응답
- 서버에 fetch API로 요청을 보내고 나면, 서버의 응답을 await 키워드로 대기
- 서버의 응답이 올 때까지 자바스크립트는 비동기적으로 대기, 응답이 오면 처리
const response = await fetch(REQUEST_URL, {
method: 'POST',
body,
...REQUEST_OPTIONS,
});
5-1. 옵션 설정
- 각 요청에는 headers와 같은 추가적인 요청 옵션이 필요
- 여기서는 Content-Type을 JSON으로 설정해 서버가 요청의 포맷을 알 수 있도록 한다.
const REQUEST_OPTIONS = {
headers: {
'Content-Type': 'application/json',
},
};
6. 에러 핸들링
- 서버에서 응답이 정상적이지 않을 때, 에러 처리(ex. 서버 오류나 네트워크 실패)
- 응답의 ok 속성을 확인하고, 요청이 실패한 경우 에러 메시지를 발생
- 에러가 발생했을 때, 직접 커스텀 에러 메시지를 포함하여 throw하는 방식으로 에러 처리
if (!response.ok) {
throw new Response(
JSON.stringify({ message: '서버에서 요청에 응답하지 않습니다.' }),
{ status: 500 }
);
}
7. 서버 응답 데이터 처리
- 서버가 정상적으로 요청을 처리한 후에는 응답 데이터를 JSON 형식으로 받아옴
- response.json()을 호출하여 데이터를 파싱한 후 반환
const responseData = await response.json();
return responseData;
☑️ 생성(Create)
// 2. 백엔드 API 엔드포인트
const ENDPOINT = 'http://127.0.0.1:8090';
// 5-1. 옵션 설정
const REQUEST_OPTIONS = {
headers: {
'Content-Type': 'application/json',
},
};
// 1. 비동기 함수 작성
/** @type {(newNote: { title: string, description: string }) => Promise<any> } */
export async function createNote(newNote) {
// 3. 외부 시스템(서버)에 데이터 생성 요청
// - POST 요청 URL
const REQUEST_URL = `${ENDPOINT}/api/collections/notes/records`;
// 4. POST 요청 시 전송할 JSON 포맷 문자열
const body = JSON.stringify({
title: newNote.title,
description: newNote.description,
});
// 5. 서버에 POST 요청 보내기
const response = await fetch(REQUEST_URL, {
method: 'POST',
body,
...REQUEST_OPTIONS,
});
// 6. 에러 핸들링
if (!response.ok) {
throw new Response(
JSON.stringify({ message: '서버에서 요청에 응답하지 않습니다.' }),
{ status: 500 }
);
}
// 7. 서버 응답 데이터 처리
const responseData = await response.json();
return responseData;
}
☑️ 읽기(Read)
/** @type {(page?: number, perPage?: number) => Promise<any>} */
export async function readNotes(page = 1, perPage = 10) {
// 서버에 GET 요청을 보낼 URL
// 페이지와 페이지당 항목 수를 쿼리 스트링으로 포함
const REQUEST_URL = `${ENDPOINT}/api/collections/notes/records?page=${page}&perPage=${perPage}`;
// 서버에 GET 요청을 보내서 데이터를 가져옴
const response = await fetch(REQUEST_URL);
// 서버 응답이 실패한 경우 에러 처리
if (!response.ok) {
// 에러 메시지를 포함한 Response 객체를 생성하고 던짐
throw new Response(
JSON.stringify({ message: '서버에서 요청에 응답하지 않습니다.' }),
{ status: 500 } // HTTP 상태 코드 500 (서버 오류)
);
}
// 서버 응답 데이터를 JSON 형식으로 파싱
const responseData = await response.json();
// 파싱된 데이터를 반환
return responseData;
}
⏩ 특정 데이터 읽기
/** @type {(noteId: string) => Promise<any>} */
export async function readNoteOne(noteId) {
// 서버에 요청할 URL
// noteId를 경로 파라미터로 사용하여 특정 노트를 조회
const REQUEST_URL = `${ENDPOINT}/api/collections/notes/records/${noteId}`;
// 서버에 GET 요청을 보내서 해당 ID의 노트를 가져옴
const response = await fetch(REQUEST_URL);
// 서버 응답이 실패한 경우 에러 처리
if (!response.ok) {
// 에러 메시지를 포함한 Response 객체를 생성하고 던짐
throw new Response(
JSON.stringify({ message: '서버에서 요청에 응답하지 않습니다.' }), // 커스텀 에러 메시지
{ status: 500 } // HTTP 상태 코드 500 (서버 오류)
);
}
// 서버 응답 데이터를 JSON 형식으로 파싱
const responseData = await response.json();
// 파싱된 데이터를 반환 (특정 노트의 데이터)
return responseData;
}
☑️ 수정(Update)
/** @type { (editNote: { id: string, title: string, description: string }) => Promise<any>} */
export async function updateNote(editNote) {
// 서버에 PATCH 요청을 보낼 URL
// - 수정할 노트의 ID를 경로에 포함
const REQUEST_URL = `${ENDPOINT}/api/collections/notes/records/${editNote.id}`;
// 요청 본문을 JSON 형식으로 변환(수정된 title과 description 포함)
const body = JSON.stringify({
title: editNote.title, // 수정할 노트의 제목
description: editNote.description, // 수정할 노트의 설명
});
// 서버에 PATCH 요청을 보내서 노트 데이터를 수정
const response = await fetch(REQUEST_URL, {
method: 'PATCH', // 데이터를 수정하기 위한 PATCH 메서드 사용
body, // JSON으로 변환한 수정된 데이터를 요청 본문에 포함
...REQUEST_OPTIONS, // 헤더 정보 포함 (JSON 형식 설정)
});
// 서버 응답이 실패한 경우 에러 처리
if (!response.ok) {
// 에러 메시지를 포함한 Response 객체를 생성하고 던짐
throw new Response(
JSON.stringify({ message: '서버에서 요청에 응답하지 않습니다.' }),
{ status: 500 } // HTTP 상태 코드 500 (서버 오류)
);
}
// 서버 응답 데이터를 JSON 형식으로 파싱
const responseData = await response.json();
// 파싱된 데이터를 반환 (수정된 노트의 데이터)
return responseData;
}
☑️ 삭제(Delete)
/** @type { (deleteId: string) => Promise<any> } */
export async function deleteNote(deleteId) {
// 서버에 DELETE 요청을 보낼 URL
// - 삭제할 노트의 ID를 경로에 포함
const REQUEST_URL = `${ENDPOINT}/api/collections/notes/records/${deleteId}`;
// 서버에 DELETE 요청을 보내서 해당 ID의 노트를 삭제
const response = await fetch(REQUEST_URL, {
method: 'DELETE', // 데이터를 삭제하기 위한 DELETE 메서드 사용
...REQUEST_OPTIONS, // 헤더 정보 포함 (JSON 형식 설정)
});
// 서버 응답이 실패한 경우 에러 처리
if (!response.ok) {
// 에러 메시지를 포함한 Response 객체를 생성하고 던짐
throw new Response(
JSON.stringify({ message: '서버에서 요청에 응답하지 않습니다.' }), // 커스텀 에러 메시지
{ status: 500 } // HTTP 상태 코드 500 (서버 오류)
);
}
// 응답 객체를 반환 (삭제된 항목의 상태를 확인하기 위한 용도)
return response;
}
✅ 기능 사용
import {
createNote,
deleteNote,
readNoteOne,
readNotes,
updateNote,
} from '@/api/notes';
import S from './DataMutation.module.css';
import { useRef } from 'react';
function DataMutation() {
// 폼 요소에 대한 참조를 저장하는 useRef 훅
const formRef = useRef(null);
// 모든 노트 읽기 함수 ---------------------
const handleReadNotes = async () => {
// readNotes 함수를 호출하여 모든 노트를 읽어옴
const responseData = await readNotes();
// 읽어온 데이터를 콘솔에 출력
console.log(responseData);
};
// 새 노트 생성 함수 ---------------------
const handleCreate = async () => {
// 폼 요소에 접근하여 FormData 객체를 생성
const formElement = formRef.current;
const formData = new FormData(formElement);
// 폼 데이터에서 입력 값을 가져옴
const title = formData.get('title');
const description = formData.get('description');
// 새 노트 객체 생성
const newNote = { title, description };
// createNote 함수를 호출하여 새 노트를 서버에 전송
const responseData = await createNote(newNote);
// 서버 응답 데이터를 콘솔에 출력
console.log(responseData);
// 응답이 성공하면 폼을 초기화
formElement.reset();
};
// 특정 ID의 노트 읽기 함수 ---------------------
const handleReadNoteOne = async () => {
// readNoteOne 함수를 호출하여 특정 노트의 데이터를 읽어옴
const responseData = await readNoteOne('i395bxkg0hqg9d1');
// 읽어온 데이터를 콘솔에 출력
console.log(responseData);
};
// 노트 데이터 수정 함수 ---------------------
const handleEditNote = async () => {
// 수정할 노트의 ID와 수정된 내용을 포함하는 객체 생성
const editNoteId = 'i395bxkg0hqg9d1';
const editNote = {
id: editNoteId,
title: '오늘도 내일도 화이팅! 🥹', // 새 제목
// description: '리액트 짱 재밌다~?!', // 새로운 설명 (주석 처리됨)
};
// updateNote 함수를 호출하여 노트 데이터를 수정
const responseData = await updateNote(editNote);
// 서버 응답 데이터를 콘솔에 출력
console.log(responseData);
};
// 노트 데이터 삭제 함수 ---------------------
const handleDeleteNote = async () => {
// 삭제할 노트의 ID
const deleteNoteId = 'i395bxkg0hqg9d1';
// deleteNote 함수를 호출하여 노트를 삭제
await deleteNote(deleteNoteId);
// 삭제 성공 알림 표시
globalThis.alert('노트 삭제 성공!');
};
return (
<div className={S.component}>
<form ref={formRef}>
<div>
<label htmlFor="noteTitle">제목</label>
<input type="text" id="noteTitle" name="title" />
</div>
<div>
<label htmlFor="noteDescription">내용</label>
<textarea
id="noteDescription"
name="description"
cols={20}
rows={3}
/>
</div>
</form>
<div
role="group"
style={{
marginBlockStart: 20,
display: 'flex',
flexFlow: 'column',
alignItems: 'start',
gap: 8,
}}
>
<button type="button" onClick={handleCreate}>
노트 작성
</button>
<button type="button" onClick={handleReadNotes}>
노트 읽기
</button>
<button type="button" onClick={handleReadNoteOne}>
노트 데이터 하나 가져오기
</button>
<button type="button" onClick={handleEditNote}>
노트 데이터 수정하기
</button>
<button type="button" onClick={handleDeleteNote}>
노트 데이터 삭제하기
</button>
</div>
</div>
);
}
export default DataMutation;