All'alba vincerò

At dawn, I will win!

React

[React] 데이터 패칭(Data Fetching) / 데이터 뮤테이션(Data Mutation)

나디아 Nadia 2024. 8. 19. 23:54

📌 데이터 패칭(Data Fetching)

: 외부 API나 데이터베이스 등에서 데이터를 가져오는(읽는) 작업

  • 서버에 데이터 요청/응답 (읽기(Read))

 

  • 비동기적
    : 네트워크 요청은 서버에서 데이터를 가져오는 동안 비동기적으로 처리됨
  • 주로 GET 요청
    : 데이터 패칭은 HTTP `GET` 요청을 통해 서버에서 데이터를 조회하는 형태
  • API 상태 관리
    : 데이터 패칭 과정에서 `isLoading, data, error` 등 API 상태를 관리해야 함
  • 서버의 데이터를 가져오는 작업은 이펙트(Effect)를 사용한다.

 

 

 

✅ 이펙트(Effect) 사용

  1. API 서버에 데이터를 요청해 응답받은 데이터를 렌더링
  2. 이펙트를 사용해 Promise 또는 Async / await를 사용해 데이터 가져오기를 요청
  3. 데이터 가져오기 요청 응답이 성공인 경우, 리액트 앱에 데이터를 렌더링
  4. 데이터 가져오기 요청 응답에 문제가 발생한 경우, 리액트 앱에 오류 메시지를 렌더링
  5. 관련없는 패치가 앱에 영향을 주지않도록 클린업 함수에서 무시하도록 설정
  6. 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) 사용

 

  1. 비동기 함수 작성: async 함수로 CRUD 작업을 비동기적으로 처리할 수 있도록 함.
  2. 백엔드 API 엔드포인트 정의: 요청할 API 서버 주소를 설정.
  3. 서버에 요청 보내기: fetch API로 서버에 요청을 전송.
  4. 전송할 데이터 준비 (POST, PATCH 요청의 경우): 데이터를 JSON 포맷으로 변환하여 전송.
  5. 서버 응답 대기 및 처리: await 키워드로 서버의 응답을 대기한 후 처리.
  6. 에러 핸들링: 응답이 성공적이지 않으면 에러를 던져 처리.
  7. 응답 데이터 반환: 서버로부터 받아온 데이터를 클라이언트에 반환.

 

 

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;

 

 

 


 

 

 

Fetch API - Web APIs | MDN

The Fetch API provides an interface for fetching resources (including across the network). It is a more powerful and flexible replacement for XMLHttpRequest.

developer.mozilla.org

 

 

Effect로 동기화하기 – React

The library for web and native user interfaces

ko.react.dev