-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 공통 API 에러 응답 스키마 구조화 #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import axios from "axios"; | ||
|
|
||
| import { parseApiError } from "@/api/error/api-error"; | ||
|
|
||
| export const instance = axios.create({ | ||
| baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, | ||
| }); | ||
|
|
||
| instance.interceptors.response.use( | ||
| (response) => response, | ||
| (error) => { | ||
| // TODO: 추후 인증 방식 확정 후 status(401 등) 기반 사이드 이펙트(리다이렉트 등) 추가 | ||
| return Promise.reject(parseApiError(error)); | ||
| }, | ||
| ); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,40 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import axios from "axios"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ApiErrorResponse, apiErrorSchema } from "@/api/schema/response"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class ApiError extends Error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly status: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly errorCode: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly path: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly timestamp: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor(response: ApiErrorResponse) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super(response.message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.name = "ApiError"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.status = response.status; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.errorCode = response.errorCode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.path = response.path; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.timestamp = response.timestamp; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function parseApiError(error: unknown): ApiError { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (axios.isAxiosError(error)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = apiErrorSchema.safeParse(error.response?.data); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (parsed.success) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new ApiError(parsed.data); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new ApiError({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp: new Date().toISOString(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| errorCode: "UNKNOWN_ERROR", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error instanceof Error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? error.message | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : "알 수 없는 오류가 발생했습니다.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path: "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+21
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win axios 에러인데 body가 스키마와 안 맞으면 실제 status/path 정보가 유실돼요.
🐛 제안 수정export function parseApiError(error: unknown): ApiError {
if (axios.isAxiosError(error)) {
const parsed = apiErrorSchema.safeParse(error.response?.data);
if (parsed.success) {
return new ApiError(parsed.data);
}
+
+ return new ApiError({
+ timestamp: new Date().toISOString(),
+ status: error.response?.status ?? 0,
+ errorCode: "UNKNOWN_ERROR",
+ message: error.message,
+ path: error.config?.url ?? "",
+ });
}
return new ApiError({
timestamp: new Date().toISOString(),
status: 0,
errorCode: "UNKNOWN_ERROR",
message:
error instanceof Error
? error.message
: "알 수 없는 오류가 발생했습니다.",
path: "",
});
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,19 @@ | ||||||
| import { z } from "zod"; | ||||||
|
|
||||||
| export function apiResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Zod 공식 마이그레이션 가이드에 따르면 ♻️ 제안 수정-export function apiResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {
+export function apiResponseSchema<T extends z.ZodType>(dataSchema: T) {지금 당장 동작에는 문제 없지만, 향후 zod 메이저 업그레이드 시 제거될 수 있는 API라 미리 정리해두면 좋을 것 같아요. 작은 수고로 미래의 골칫거리를 예방하는 셈이죠! 😄 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| return z.object({ | ||||||
| status: z.number(), | ||||||
| message: z.string(), | ||||||
| data: dataSchema, | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| export const apiErrorSchema = z.object({ | ||||||
| timestamp: z.string(), | ||||||
| status: z.number(), | ||||||
| errorCode: z.string(), | ||||||
| message: z.string(), | ||||||
| path: z.string(), | ||||||
| }); | ||||||
|
|
||||||
| export type ApiErrorResponse = z.infer<typeof apiErrorSchema>; | ||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
axios 인스턴스에 timeout이 설정되어 있지 않아요.
응답이 지연되거나 네트워크가 멈추면 요청이 무한정 대기하게 되고, 이 상태에서는
parseApiError가 실행될 기회조차 없어 사용자에게 로딩 상태만 남게 됩니다. 인스턴스 레벨에 기본 timeout을 설정해두면 이런 행잉을 방지할 수 있어요.🛡️ 제안 수정
export const instance = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, + timeout: 10000, });작은 설정 한 줄로 사용자 경험이 확 달라질 수 있는 포인트라 놓치지 않았으면 해요!
📝 Committable suggestion
🤖 Prompt for AI Agents