import React, { useEffect, useState } from "react"; const CartBadge: React.FC = () => { const [cartCount, setCartCount] = useState(0); useEffect(() => { fetch("https://api.baemin.com/cart") .then((response) => response.json()) .then(({ cartItem }) => { setCartCount(cartItem.length); }); }, []); return {/* cartCount 상태를 이용하여 컴포넌트 렌더링 */}</>; };
모든 api에서 동일한 추가 기능(커스텀 헤더, 타임아웃 등)을 원할경우 모든 api 요청 함수를 찾고, 하나하나 수정해야함. -> 비효율적
// 이 코드 또한 변경사항을 하나하나 넣어야함으로 비효율적이다. async function fetchCart() { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); const response = await fetch("https://api.baemin.com/cart", { signal: controller.signal, }); clearTimeout(timeoutId); return response; }
fetch 는 기본 내장 라이브러리라 추가 기능 하려면 직접구현해야함 -> 그거 귀찮아서 Axios 쓴다.
api entry 가 여러개면 instance 여러개 만들면 됨
// 각가 다른 헤더를 넣는 인터셉트 import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; const getUserToken = () => ""; const getAgent = () => ""; const getOrderClientToken = () => ""; const orderApiBaseUrl = ""; const orderCartApiBaseUrl = ""; const defaultConfig = {}; const httpErrorHandler = () => {}; const apiRequester: AxiosInstance = axios.create({ baseURL: "https://api.baemin.com", timeout: 5000, }); const setRequestDefaultHeader = (requestConfig: AxiosRequestConfig) => { const config = requestConfig; config.headers = { ...config.headers, "Content-Type": "application/json;charset=utf-8", user: getUserToken(), agent: getAgent(), }; return config; }; const setOrderRequestDefaultHeader = (requestConfig: AxiosRequestConfig) => { const config = requestConfig; config.headers = { ...config.headers, "Content-Type": "application/json;charset=utf-8", "order-client": getOrderClientToken(), }; return config; }; // `interceptors` 기능을 사용해 header를 설정하는 기능을 넣거나 에러를 처리할 수 있다 apiRequester.interceptors.request.use(setRequestDefaultHeader); const orderApiRequester: AxiosInstance = axios.create({ baseURL: orderApiBaseUrl, ...defaultConfig, }); // 기본 apiRequester와는 다른 header를 설정하는 `interceptors` orderApiRequester.interceptors.request.use(setOrderRequestDefaultHeader); // `interceptors`를 사용해 httpError 같은 API 에러를 처리할 수도 있다 orderApiRequester.interceptors.response.use( (response: AxiosResponse) => response, httpErrorHandler ); const orderCartApiRequester: AxiosInstance = axios.create({ baseURL: orderCartApiBaseUrl, ...defaultConfig, }); orderCartApiRequester.interceptors.request.use(setRequestDefaultHeader);
그 아래는 이런식으로 api 호출은 우리는 만들어서 쓴다.
interface Response<T> { data: T; status: string; serverDateTime: string; errorCode?: string; // FAIL, ERROR errorMessage?: string; // FAIL, ERROR }
아래 형식으로 지정해서 그냥 다 묶어 버리면 편하다.
근데 update나 create는 데이터를 받는게 아니라 보내는 거니깐 굳이 데이터를 받을 필요가 없다.
근데 위 형식으로 묶어버리면 불필요한 데이터를 받아야 한다는거지? 그게 싫다.
그러니 세부 데이터 타입 설정은 쓰는 데서 해라.
interface response { data: { cartItems: CartItem[]; forPass: unknown; }; } // 필요하면 아래처럼 type ForPass = { type: "A" | "B" | "C"; }; const isTargetValue = () => (data.forPass as ForPass).type === "A";
장점
근데 test = items 로 하고 test.ㅇㅇㅇ으로 쓰고 있다면
test = items를 test = lists 로만 하면 된다는거임.
프론트에서만 추가하고 싶은거있으면 그냥 뷰모델 부분에 추가하고.
단점
근데 인놈 여러군데서 쓰면 코드가 복잡해지고, 관리 개발도 쉽지 않음.... 그래서 일단 뷰모델 적게 쓰고, 백엔드랑 잘~ 이야기해서 변경점 최대한 적게, 그리고 뷰모델쓰면 어디서 끌어왔는지 달아주기 ㄱㄱ
너거 Superstruct 이거 써봄? 이거 런타임에 데이터 타입 검증해줌 어케쓰는지 함 볼래? 아래는 예시 코드임
import { assert, is, validate, object, number, string, array, } from "superstruct"; const Article = object({ id: number(), title: string(), tags: array(string()), author: object({ id: number(), }), }); const data = { id: 34, title: "Hello World", tags: ["news", "features"], author: { id: 1, }, }; assert(data, Article); is(data, Article); validate(data, Article);
야 이거 함 봐라
const Article = object({ }) 요 안에 받는 데이터형식 넣어주고,
assert는 아니다 싶으면 에러를 던져.
is는 통과했는지 boolean 값을 반환하고,
validate는 [error, data] 형식의 튜플을 반환한다. 유효하지 않을 때는 에러 값이 반환되고, 유효한 경우에는 첫 번째 요소로 undefined, 두 번째 요소로 data value가 반환된다.
자 그럼 이거 타입스크립트랑 어케쓰는데? 어케쓰긴 data 값에다가 내가 원하는 값의 타입 박아넣고 매칭되는지 확인하면 되지롱
아니면 아니라도 에러뜰꺼아니아 데이터 이런식으로 오는데, 느가 적은건 이래. 정적말고 동적으로도 확인 가능하다~ 이거야
interface ListItem { id: string; content: string; } interface ListResponse { items: ListItem[]; } const fetchList = async (filter?: ListFetchFilter): Promise<ListResponse> => { const { data } = await api .params({ ...filter }) .get("/apis/get-list-summaries") .call<Response<ListResponse>>(); return { data }; }; import { assert } from "superstruct"; function isListItem(listItems: ListItem[]) { listItems.forEach((listItem) => assert(listItem, ListItem)); }
보통 api는 컴포넌트에서 안부름. 훅이나 라이브러리에서 가공해서 씀
상태 관리 라이브러리의 비동기 함수들은 비동기 상태를 변화 시키는 코드를 가지고 있음. 컴포넌트는 비동기 함수가 아닌 상태관리 라이브러리의 바뀐 상태를 가지고 와서 렌더링한다.
스토어에서 api를 호출하고 그 결과를 받아와서 상태를 변환 시키는 것이 일반적이나, 호출할때 , 호출한 뒤 , 에러가 발생했을시 상태를 업데이트 해야한다.
useSelector로 현재 상태 읽기.useDispatch로 액션을 날려 상태 변경.setApiCall 같은 액션을 여러 번 호출.status: Request, 성공하면 status: None, 실패해도 status: None으로 변경하는 코드를 매번 작성. => 보일러 플레이트 많음makeAutoObservable로 스토어 생성 → 상태와 메서드 자동 반응형 처리.state = "PENDING", 성공하면 "DONE", 실패하면 "ERROR".runInAction을 써서 비동기 콜백 내부에서 안전하게 상태 변경.async/await나 flow를 활용해 비동기 상태를 직관적으로 작성 가능.create()로 스토어 생성 → 상태/액션을 한 곳에 정의.set()으로 상태 갱신.useStore(selector)로 필요한 조각만 구독 → 불필요 리렌더 줄이기.persist, immer, devtools, subscribeWithSelector.// store.ts (Zustand)
import { create } from 'zustand';
type Item = { id: number; name: string };
type State = {
items: Item[];
loading: boolean;
error?: string;
fetchItems: () => Promise<void>;
};
export const useItemStore = create<State>((set, get) => ({
items: [],
loading: false,
async fetchItems() {
set({ loading: true, error: undefined });
try {
const res = await fetch('/api/items').then(r => r.json());
set({ items: res.items, loading: false });
} catch (e: any) {
set({ loading: false, error: e?.message ?? 'error' });
}
},
}));
// 컴포넌트
const items = useItemStore(s => s.items);
const loading = useItemStore(s => s.loading);
loadable 유틸로 관리(필요시 Suspense 연계).loadable, atomFamily).// atoms.ts (Jotai)
import { atom } from 'jotai';
import { loadable } from 'jotai/utils';
type Item = { id: number; name: string };
const itemsAtom = atom(async () => {
const res = await fetch('/api/items').then(r => r.json());
return res.items as Item[];
});
// 로딩/에러 상태를 명시적으로 다루고 싶다면:
export const itemsLoadableAtom = loadable(itemsAtom);
// 컴포넌트
const loadableItems = useAtomValue(itemsLoadableAtom);
if (loadableItems.state === 'loading') return <Spinner/>;
if (loadableItems.state === 'hasError') return <Error msg={String(loadableItems.error)}/>;
return <List data={loadableItems.data}/>;
리액트 쿼리나 useSwr 훅은 캐시를 사용해서 비동기 함수를 호출함 의도치 않은 상태 변경 방지가 도움됨
// Job 목록을 불러오는 훅
const useFetchJobList = () => {
return useQuery(["fetchJobList"], async () => {
const response = await JobService.fetchJobList(); // View Model을 사용해서 결과
return new JobList(response);
});
};
// Job 1개를 업데이트하는 훅
const useUpdateJob = (
id: number,
{ onSuccess, ...options }: UseMutationOptions<void, Error, JobUpdateFormValue>
) => {
const queryClient = useQueryClient();
return useMutation(
["updateJob", id], // mutation key
async (jobUpdateForm: JobUpdateFormValue) => {
await JobService.updateJob(id, jobUpdateForm); // API 호출 (PUT/PATCH)
},
{
onSuccess: (
data: void, // updateJob의 return 값은 없다 (status 200으로만 성공 판별)
values: JobUpdateFormValue,
context: unknown
) => {
// 'fetchJobList' 캐시 무효화 → 다음 조회 시 최신 데이터로 갱신
// 성공 시 ‘fetchJobList’를 유효하지 않음으로 설정
queryClient.invalidateQueries(["fetchJobList"]);
// 옵션으로 받은 onSuccess 콜백 실행
onSuccess && onSuccess(data, values, context);
},
...options,
}
);
};
const JobList: React.FC = () => {
// 비동기 데이터를 필요한 컴포넌트에서 자체 상태로 저장
const {
isLoading,
isError,
error,
refetch,
data: jobList,
} = useFetchJobList();
// 간단한 Polling 로직, 실시간으로 화면이 갱신돼야 하는 요구가 없어서 // 30초 간격으로 갱신한다
useInterval(() => refetch(), 30000);
// Loading인 경우에도 화면에 표시해준다
if (isLoading) return <LoadingSpinner />;
// Error에 관한 내용은 11.3 API 에러 핸들링에서 더 자세하게 다룬다
if (isError) return <ErrorAlert error={error} />;
return (
<>
{jobList.map((job) => (
<Job job={job} />
))}
</>
);
};
비동기는 리액트 쿼리가 좋다.