AS IS
Django DRF + React axios zustand 프로젝트에서 내가 사용한 Django 응답 구조는 다음과 같다
Response(
dict(
success=False,
message="실패...",
errors=serializer.errors,
),
status=status.HTTP_400_BAD_REQUEST,
)- pros:
success,message,errors가 한눈에 보여서 React 쪽에서 처리하기 편함
- cons:
- DRF의 기본 pagination, exception, handler, status 코드 일관성과 약간 충돌
- pagination이 자동으로 적용되지 않음
- DRF의 다른 기능(
ListAPIView,ModleViewSet등)과 통일되지 않음
즉 “일관된 DRF 방식” 과 다르게 수동으로 Response를 포장했기 때문에 장단점이 섞여있음
TO BE
RESTful DRF 방식
성공응답:
# ViewSet / GenericAPIview
return Response(serializer.data) # status 기본 200Validation Error: DRF가 자동으로 처리
{
"username": ["This field is required."]
}Pagination:
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
pagination_class = PageNumberPagination-> 자동으로
{
"count": 123,
"next": "http://.../page=2",
"previous": null,
"results": [...]
}특징:
- 별도의
success나message키 없이 HTTP 상태 코드만으로 성공/실패 판단 가능 - pagination, filtering, ordering 등이 기본 기능으로 자동 적용
- React 쪽에서
res.status와res.data로 쉽게 처리 가능
DRF View & Pagination
# views.py
from rest_framework import generics
from rest_framework.pagination import PageNumberPagination
from .models import User
from .serializers import UserSerializer
class StandardResultsSetPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
class UserListView(generics.ListAPIView):
queryset = user.objects.all()
serializer_class = UserSerializer
pagination_class = StandardResultsSetPagination- 이 구조대로 하면 DRF가 자동으로
count,next,previous,results형태의 pagination JSON을 만들어줌 - Validation Error도 serializer에서 자동으로 처리됨
Axios 설정 (interceptor)
// apiClient.ts
import axios from 'axios';
const api = axios.create({
baseURL: "/api",
withCredentials: true,
});
api.interceptors.response.use(
(response) => response, // 성공 응답은 그대로
(error) => {
// 에러 공통 처리
const data = error.response?.data;
const status = error.response?.status;
// 예: 401 Unauthorized 처리
if (status === 401) {
console.log("로그인 필요");
}
return Promise.reject(error);
}
);
export default api;Zustand Store
// useUserStore.ts
import { create } from "zustand";
import api from "./apiClient";
interface Pagination {
count: number;
next: string | null;
previous: string | null;
}
interface UserState {
users: any[];
pagination: Pagination | null;
loading: boolean;
error: any | null;
fetchUsers: (page?: number) => Promise<void>;
}
export const useUserStore = create<UserState>((set) => ({
users: [],
pagination: null,
loading: false,
error: null,
fetchUsers: async (page = 1) => {
set({ loading true, error: null });
try {
cosnt res = await api.get(`/users/?page=${page}`);
set({
users: res.data.results,
pagination: {
count: res.data.count,
next: res.data.next,
previous: res.data.previous,
},
loading: false,
});
} catch (err: any) {
set({ error: err.response?.data, loading: false});
}
},
}));loading상태 -> 호출 중이면trueusers-> 현재 페이지 데이터pagination->count,next,previouserror-> validation 에러나 서버 에러 그대로
React 컴포넌트 사용 예시
import { useEffect } from "react";
import { useUserStore } from "./useUserStore";
export default function UserList() {
const { users, pagination, loading, error, fetchUsers } = useUserStore();
useEffect(() => {
fetchUsers();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p style={{ color: "red"}}>{JSON.stringify(error)}</p>;
return (
<div>
<ul>
{users.map((user) => (
<li key={user.id}>{user.username}</li>
))}
</ul>
<div>
{pagination?.previous && (
<button onClick={() => fetchUsers(new URL(pagination.previous).searchParams.get("page"))}>
이전
</button>
)}
{pagination?.next && (
<button onClick={() => fetchUsers(new URL(pagination.next).searchParams.get("page"))}>
다음
</button>
)}
</div>
</div>
)
}pagination.next/pagination.previous를 이용해 페이지 이동 가능- validation error나 서버 에러도
error에서 그대로 표시 - loading 상태에 따라 로딩 UI 표시
이 구조의 장점:
- DRF 기본 RESTful 응답 그대로 활용 -> DRF 기능 최대 활용
- React + Zustand에서 상태 관리 깔끔하게 통합
- loading / error / pagination까지 한눈에 관리 가능
- 필요하면 Axios 인터셉터로 인증/공통 에러 처리 확장 가능