HTTP, json-server, TanStack Query
클라이언트-서버 모델 기반 동작(클라이언트가 요청 서버가 응답 반환)
: 서버가 클라이언트의 요청을 처리한 결과를 나타냄
상태코드는 세 자리 숫자로 구성, 첫 번째 자리에 따라 의미가 달라짐
HTTP 메서드를 클라이언트가 서버에게 요청의 성격을 알리는데 사용
REST API는 HTTP 메서드를 사용하여 CRUD 작업을 수행
엔드포인트
root 경로에 db.json을 만들어줌
yarn add json-server
Root에 db.json 생성 후 초기 값 작성
{
"todos": [
{
"id": 1,
"title": "json-server",
"content": "json-server를 배워봅시다."
}
],
"users": []
}
yarn json-server db.json --port 4000
구동연결시켜줌
yarn json-server db.json --port 4000
간단한 DB완성
Axios란?
: node.js와 브라우저를 위한 Promise 기반 http 클라이언트
http를 이용해서 서버와 통신하기 위해 사용하는 패키지
fetch처럼 데이터를 불러올 수 있다.
fetch처럼 한번 더 데이터를 변환할 필요가 없음.
yarn add axios
get은 서버의 데이터를 조회할 때 사용
axios.get(url[,config])
// src/App.js
import React, { useEffect, useState } from "react";
import axios from "axios"; // axios import 합니다.
const App = () => {
const [todos, setTodos] = useState(null);
// axios를 통해서 get 요청을 하는 함수를 생성합니다.
// 비동기처리를 해야하므로 async/await 구문을 통해서 처리합니다.
const fetchTodos = async () => {
const { data } = await axios.get("http://localhost:4000/todos");
setTodos(data); // 서버로부터 fetching한 데이터를 useState의 state로 set 합니다.
};
// 생성한 함수를 컴포넌트가 mount된 후 실행하기 위해 useEffect를 사용합니다.
useEffect(() => {
// effect 구문에 생성한 함수를 넣어 실행합니다.
fetchTodos();
}, []);
// data fetching이 정상적으로 되었는지 콘솔을 통해 확인합니다.
console.log(todos);
return <div>App</div>;
};
export default App;
axios.post(url[,data[,config]])
보통 서버에 데이터를 추가할 때 사용
// src/App.jsx
import React, { useEffect, useState } from "react";
import axios from "axios"; // axios import 합니다.
const App = () => {
// 새롭게 생성하는 todo를 관리하는 state
const [todo, setTodo] = useState({
title: "",
});
const [todos, setTodos] = useState(null);
const fetchTodos = async () => {
const { data } = await axios.get("http://localhost:4000/todos");
setTodos(data);
};
// (원리를 꼭 이해합시다!)
// HTTP에서는 body에 javascript 객체를 direct로 넣을 수 없어요!
// axios는 내부적으로 JSON.stringify를 적용하기 때문에 이처럼 편리하게 사용하는 것 뿐입니다.
const onSubmitHandler = async(todo) => {
await axios.post("http://localhost:4000/todos", todo);
};
// 만일 fetch를 사용했다면, 이렇게 JSON.stringify를 '직접' 해주어야 해요.
// await fetch("http://localhost:4000/todos", {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify(todo),
// });
useEffect(() => {
fetchTodos();
}, []);
return (
<>
<form
onSubmit={(e) => {
// 👇 submit했을 때 브라우저의 새로고침을 방지합니다.
e.preventDefault();
onSubmitHandler(todo);
}}
>
<input
type="text"
onChange={(ev) => {
const { value } = ev.target;
setTodo({
...todo,
title: value,
});
}}
/>
<button>추가하기</button>
</form>
<div>
{todos?.map((todo) => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
</>
);
};
export default App;
: DELETE는 저장되어 있는 데이터를 삭제하고자 요청을 보낼 때 사용
axios.delete(url[,config])
// src/App.jsx
import React, { useEffect, useState } from "react";
import axios from "axios";
const App = () => {
const [todo, setTodo] = useState({
title: "",
});
const [todos, setTodos] = useState(null);
const fetchTodos = async () => {
const { data } = await axios.get("http://localhost:4000/todos");
setTodos(data);
};
const onSubmitHandler = (todo) => {
axios.post("http://localhost:4000/todos", todo);
};
// 새롭게 추가한 삭제 버튼 이벤트 핸들러
const onClickDeleteButtonHandler = (todoId) => {
axios.delete(`http://localhost:4000/todos/${todoId}`);
};
useEffect(() => {
fetchTodos();
}, []);
return (
<>
<form
onSubmit={(e) => {
e.preventDefault();
onSubmitHandler(todo);
}}
>
<input
type="text"
onChange={(ev) => {
const { value } = ev.target;
setTodo({
...todo,
title: value,
});
}}
/>
<button>추가하기</button>
</form>
<div>
{todos?.map((todo) => (
<div key={todo.id}>
{todo.title}
<button
type="button"
onClick={() => onClickDeleteButtonHandler(todo.id)}
>
삭제하기
</button>
</div>
))}
</div>
</>
);
};
export default App;
: 어떤 데이터를 수정하고자 서버에 요청을 보낼 때 사용하는 메서드
axios.patch(url[,data[,config]])
// src/App.jsx
import React, { useEffect, useState } from "react";
import axios from "axios";
const App = () => {
const [todo, setTodo] = useState({
title: "",
});
const [todos, setTodos] = useState(null);
// patch에서 사용할 id, 수정값의 state를 추가
const [targetId, setTargetId] = useState(null);
const [editTodo, setEditTodo] = useState({
title: "",
});
const fetchTodos = async () => {
const { data } = await axios.get("http://localhost:4000/todos");
setTodos(data);
};
const onSubmitHandler = (todo) => {
axios.post("http://localhost:4000/todos", todo);
};
const onClickDeleteButtonHandler = (todoId) => {
axios.delete(`http://localhost:4000/todos/${todoId}`);
};
// 수정버튼 이벤트 핸들러 추가 👇
const onClickEditButtonHandler = (todoId, edit) => {
axios.patch(`http://localhost:4000/todos/${todoId}`, edit);
};
useEffect(() => {
fetchTodos();
}, []);
return (
<>
<form
onSubmit={(e) => {
e.preventDefault();
onSubmitHandler(todo);
}}
>
{/* 👇 수정기능에 필요한 id, 수정값 input2개와 수정하기 버튼을 추가 */}
<div>
<input
type="text"
placeholder="수정하고싶은 Todo ID"
onChange={(ev) => {
setTargetId(ev.target.value);
}}
/>
<input
type="text"
placeholder="수정값 입력"
onChange={(ev) => {
setEditTodo({
...editTodo,
title: ev.target.value,
});
}}
/>
<button
// type='button' 을 추가해야 form의 영향에서 벗어남
type="button"
onClick={() => onClickEditButtonHandler(targetId, editTodo)}
>
수정하기
</button>
</div>
<input
type="text"
onChange={(ev) => {
const { value } = ev.target;
setTodo({
...todo,
title: value,
});
}}
/>
<button>추가하기</button>
</form>
<div>
{todos?.map((todo) => (
<div key={todo.id}>
{/* todo의 아이디를 화면에 표시 */}
{todo.id} :{todo.title}
<button
type="button"
onClick={() => onClickDeleteButtonHandler(todo.id)}
>
삭제하기
</button>
</div>
))}
</div>
</>
);
};
export default App;
fetch와 axios 모두 HTTP요청을 처리하기 위한 JavaScript 라이브러리이다.
기본설정 : axios는 기본설정을 정의하고 이를 통해 모든 요청에 공통 설정을 적용할 수 있다.
인터셉터 : 요청 또는 응답을 가로채서 전처리 또는 후처리 할 수 있다.
이를 통해 인증 토큰을 자동으로 추가하거나 오류를 일괄 처리할 수 있다.
Redux 미들웨어
action객체가 아닌 thunk함수를 dispatch 해줌
: 서버상태관리 도구
클라이언트 상태
는 UI 관련된 일시적인 데이터(ex: 폼 입력값 )을 의미
서버 상태
는 서버에서 가져오는 데이터를 포함하며, 캐싱, 동기화, 재검증 등의 복잡한 관리가 필요
(외부에서 가져오는 데이터를 어떻게 갱신하고 관리할 지 )
get - useQuery
modify - useMutaion
refresh - InvalidateQuery
useQuery는 데이터를 가져오기 위해 사용되는 TanStack Query의 대표적인 훅. 쿼리 키와 비동기 함수(패칭 함수)를 인자로 받아 데이터를 가져오고, 로딩 상태, 오류 상태, 그리고 데이터를 반환
yarn add @tanstack/react-query
App.jsx나 main.jsx에서
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
createRoot(document.getElementById("root")).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StrictMode>
);
useMutation
은 데이터를 생성, 수정, 삭제하는 등의 작업에 사용되는 훅 CUD에 대한 비동기 작업을 쉽게 수행하고, 성공 또는 실패 시에 추가적인 작업을 실행할 수 있기 때문에 useQuery
와 함께 가장 대표적인 TanStack Query hook이라고 할 수 있음import { useMutation, useQuery } from "@tanstack/react-query";
import axios from "axios";
import { useState } from "react";
const App = () => {
const [todoItem, setTodoItem] = useState("");
const fetchTodos = async () => {
const response = await axios.get("http://localhost:4000/todos");
return response.data;
};
const addTodo = async (newTodo) => {
await axios.post("http://localhost:4000/todos", newTodo);
};
const {
data: todos,
isPending,
isError,
} = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
const { mutate } = useMutation({
mutationFn: addTodo,
});
if (isPending) {
return <div>로딩중입니다...</div>;
}
if (isError) {
return <div>데이터 조회 중 오류가 발생했습니다.</div>;
}
return (
<div>
<h3>TanStack Query</h3>
<form
onSubmit={(e) => {
e.preventDefault();
const newTodoObj = { title: todoItem, isDone: false };
// useMutation 로직 필요
mutate(newTodoObj);
}}
>
<input
type="text"
value={todoItem}
onChange={(e) => setTodoItem(e.target.value)}
/>
<button>추가</button>
</form>
<ul>
{todos.map((todo) => {
return (
<li
key={todo.id}
style={{
display: "flex",
alignItems: "center",
gap: "10px",
backgroundColor: "aliceblue",
}}
>
<h4>{todo.title}</h4>
<p>{todo.isDone ? "Done" : "Not Done"}</p>
</li>
);
})}
</ul>
</div>
);
};
export default App;
: invalidateQueries는 특정 쿼리를 무효화하여 데이터를 다시 패칭하게 하는 함수. 주로 useMutation과 함께 사용하여 데이터가 변경된 후 관련 쿼리를 다시 가져오도록 함
: 이를 통해 데이터가 항상 최신 상태로 유지될 수 있도록 도와줌. 예를 들어, 새로운 할 일을 추가한 후 기존의 할 일 목록을 다시 가져오도록 할 수 있음
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
import { useState } from "react";
const App = () => {
const queryClient = useQueryClient();
const [todoItem, setTodoItem] = useState("");
const fetchTodos = async () => {
const response = await axios.get("http://localhost:4000/todos");
return response.data;
};
const addTodo = async (newTodo) => {
await axios.post("http://localhost:4000/todos", newTodo);
};
const {
data: todos,
isPending,
isError,
} = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
const { mutate } = useMutation({
mutationFn: addTodo,
onSuccess: () => {
// alert("데이터 삽입이 성공했습니다.");
queryClient.invalidateQueries(["todos"]);
},
});
if (isPending) {
return <div>로딩중입니다...</div>;
}
if (isError) {
return <div>데이터 조회 중 오류가 발생했습니다.</div>;
}
return (
<div>
<h3>TanStack Query</h3>
<form
onSubmit={(e) => {
e.preventDefault();
const newTodoObj = { title: todoItem, isDone: false };
// useMutation 로직 필요
mutate(newTodoObj);
}}
>
<input
type="text"
value={todoItem}
onChange={(e) => setTodoItem(e.target.value)}
/>
<button>추가</button>
</form>
<ul>
{todos.map((todo) => {
return (
<li
key={todo.id}
style={{
display: "flex",
alignItems: "center",
gap: "10px",
backgroundColor: "aliceblue",
}}
>
<h4>{todo.title}</h4>
<p>{todo.isDone ? "Done" : "Not Done"}</p>
</li>
);
})}
</ul>
</div>
);
};
export default App;
일단은 원래의 데이터를 쓰고 새로운 데이터가 오면 갈아끼워줌
QueryClientProvider를 사용하여 React 애플리케이션 전체에서 캐시 데이터에 접근
내부적으로 React Context API를 사용한다.
staleTime === 0
얼마나 신선한 상태를 유지할 것인가!
데이터가 캐시되고, 사용되고, 갱신되는 과정
default config(기본 설정)
staleTime : 0
refetchOnMount : true
refetchOnWindowFocus:true
refetchOnRecconnect : true
geTime(cacheTime) : 5분
retry : 3