Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions lib/axios.ts

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File Walkthrough

Enhancement
axios.ts (+42/-0)
클라이언트용 Axios 인스턴스 및 토큰 재발급 인터셉터                                                   

lib/axios.ts

  • Axios 클라이언트 인스턴스 생성 및 공통 설정 (baseURL, 헤더, withCredentials)
  • 응답 인터셉터로 401 에러 감지 시 토큰 자동 재발급 로직 구현
  • 재발급 실패 시 로그인 페이지로 리다이렉트 처리
  • 재시도 플래그(_retry)로 무한 루프 방지
server-api.ts (+130/-0)
서버 환경 API 클라이언트 및 쿠키 기반 인증                                                             

lib/server-api.ts

  • 서버 환경 전용 Axios 인스턴스 생성 (타임아웃 설정)
  • 요청 인터셉터에서 쿠키 저장소에서 인증 쿠키만 선별하여 헤더에 추가
  • 응답 인터셉터로 401 에러 시 토큰 재발급 및 원래 요청 재시도
  • 제네릭 request() 함수와 HTTP 메서드별 래퍼 함수(get, post, put, delete, patch) 제공
  • 에러 로깅 및 상세한 에러 정보 전달
Dependencies
package.json (+3/-0)
API 관리 및 개발 도구 의존성 추가                                                                       

package.json

  • @tanstack/react-query ^5.90.20 추가
  • @tanstack/react-query-devtools ^5.91.2 추가
  • axios ^1.13.3 추가
pnpm-lock.yaml (+113/-0)
의존성 잠금 파일 업데이트                                                                                     

pnpm-lock.yaml

  • TanStack Query 관련 패키지 잠금 버전 추가 (query-core, react-query,
    react-query-devtools)
  • Axios 및 관련 의존성 잠금 버전 추가 (follow-redirects, form-data, proxy-from-env)
  • 부가 의존성 추가 (asynckit, combined-stream, delayed-stream, mime-types,
    mime-db)

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import axios from "axios";

// 1. 공통 설정 (URL, 헤더 등)
const axiosConfig = {
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: { "Content-Type": "application/json" },
withCredentials: true,
};

// 2. 클라이언트용(CSR) 인스턴스 생성 및 export
export const api = axios.create(axiosConfig);

// 3. 응답 인터셉터: 401 에러 시 토큰 자동 재발급
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;

// 401 에러 && 재발급 시도 전 && 재발급 API가 아닐 때
if (
error.response?.status === 401 &&
!originalRequest._retry &&
originalRequest.url !== "/api/auth/login"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

인증 관련 경로("/api/auth/login")가 코드 내에 하드코딩되어 있습니다. 이러한 문자열 리터럴은 변경될 가능성이 있으며, 여러 곳에서 사용될 경우 일관성 유지에 어려움이 있습니다. 이를 별도의 상수로 분리하여 관리하면 코드의 유연성과 유지보수성이 향상됩니다.

Suggested change
originalRequest.url !== "/api/auth/login"
originalRequest.url !== AUTH_LOGIN_API_PATH

) {
originalRequest._retry = true;

try {
// refreshToken으로 새 accessToken 발급 (withCredentials로 쿠키 자동 전송)
await api.post("/api/auth/login");

// 원래 요청 재시도
return api(originalRequest);
} catch (reissueError) {
// 재발급 실패 → 로그인 페이지로 리다이렉트
window.location.href = "/login";
return Promise.reject(reissueError);
}
}

return Promise.reject(error);
},
);
130 changes: 130 additions & 0 deletions lib/server-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import axios, { type AxiosRequestConfig, isAxiosError } from "axios";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

const API_URL = process.env.NEXT_PUBLIC_API_URL;
if (!API_URL) {
throw new Error("NEXT_PUBLIC_API_URL is not defined");
}

// 1. 서버 전용 Axios 인스턴스 생성
const serverClient = axios.create({
baseURL: API_URL,
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
});

// 2. [핵심] 요청 인터셉터: 인증에 필요한 쿠키만 선별하여 전달
serverClient.interceptors.request.use(
async (config) => {
const cookieStore = await cookies();

// 백엔드 API 인증에 필요한 쿠키만 선별
const authCookieNames = ["accessToken", "refreshToken", "sessionId"];
const authCookies: string[] = [];

authCookieNames.forEach((name) => {
const cookie = cookieStore.get(name);
if (cookie) {
authCookies.push(`${name}=${cookie.value}`);
}
});

// 인증 쿠키가 있을 때만 헤더에 추가
if (authCookies.length > 0) {
config.headers.Cookie = authCookies.join("; ");
}
Comment on lines +22 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

cookieStore.toString()을 사용하여 모든 쿠키를 API 요청 헤더에 그대로 전달하는 방식은 보안 및 성능상 잠재적인 위험을 가집니다.

이 방식은 API 인증과 무관한 제3자 서비스(예: 분석 도구)의 쿠키까지 백엔드 서버로 전송하여 불필요한 데이터를 노출할 수 있으며, 헤더 크기 제한을 초과할 위험도 있습니다.

필요한 세션 또는 인증 토큰 쿠키(예: accessToken)만 선별하여 전달하는 방식으로 리팩터링하는 것을 강력히 권장합니다.

Suggested change
const cookieStore = await cookies();
const allCookies = cookieStore.toString(); // "key=value; key2=value2" 형태
if (allCookies) {
config.headers.Cookie = allCookies;
}
const cookieStore = await cookies();
const sessionCookie = cookieStore.get("session-token"); // 예시: 실제 사용하는 인증 쿠키 이름으로 변경하세요.
if (sessionCookie) {
config.headers.Cookie = `${sessionCookie.name}=${sessionCookie.value}`;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정완료


return config;
},
(error) => Promise.reject(error),
);

// 3. [핵심] 응답 인터셉터: 401 에러 시 토큰 재발급 시도
serverClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;

// 401 에러 && 재발급 시도 전 && 재발급 API가 아닐 때
if (
error.response?.status === 401 &&
!originalRequest._retry &&
originalRequest.url !== "/api/auth/login"
) {
originalRequest._retry = true;

try {
// refreshToken으로 새 accessToken 발급 (쿠키 자동 전송됨)
await serverClient.post("/api/auth/login");

// 원래 요청 재시도
return serverClient(originalRequest);
} catch (reissueError) {

Check warning on line 65 in lib/server-api.ts

View workflow job for this annotation

GitHub Actions / lint

'reissueError' is defined but never used
// 재발급 실패 → 로그인 페이지로
redirect("/login");
}
}

return Promise.reject(error);
},
);

// 요청 옵션 타입 정의
type RequestOptions = {
path: string;
params?: Record<string, string | number | boolean>; // 쿼리 파라미터
headers?: Record<string, string>; // 추가 헤더
body?: unknown; // POST, PUT 등에서 보낼 데이터
};

// 3. 통합 요청 함수
async function request<T>(
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
{ path, params, headers, body }: RequestOptions,
): Promise<T> {
// Axios 설정 객체
const config: AxiosRequestConfig = {
url: path,
method,
headers,
params, // Axios가 객체를 쿼리스트링(?key=value)으로 자동 변환해줍니다. (buildQuery 불필요)
data: body,
};

try {
const response = await serverClient.request<T>(config);
return response.data;
} catch (error) {
if (isAxiosError(error)) {
const status = error.response?.status;
const errorMessage = error.response?.data?.message || "API Error";

console.error(`[Server API Error] ${method} ${path}`, {
status,
message: errorMessage,
data: error.response?.data,
});
Comment on lines +105 to +109
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

console.error를 사용하여 에러를 로깅하는 것은 개발 환경에서는 유용하지만, 프로덕션 환경에서는 Sentry와 같은 중앙 집중식 로깅/에러 모니터링 솔루션을 사용하는 것이 더 효과적입니다. 현재 구현은 기능적으로 문제가 없으나, 향후 운영을 위해 고려해볼 사항입니다.

Suggested change
console.error(`[Server API Error] ${method} ${path}`, {
status,
message: errorMessage,
data: error.response?.data,
});
// 프로덕션 환경에서는 Sentry 등 중앙 집중식 로깅 솔루션 사용을 고려
console.error(`[Server API Error] ${method} ${path}`, {
status,
message: errorMessage,
data: error.response?.data,
});


// 원본 에러를 그대로 던져서 상위에서 전체 컨텍스트 활용 가능
// (401은 이미 인터셉터에서 처리됨)
throw error;
}

// Axios 외의 알 수 없는 에러
throw error;
}
}

// 4. 사용하기 편하게 메서드별 export
export const serverApi = {
// GET 요청은 body가 없으므로 Omit으로 타입 제외
get: <T>(options: Omit<RequestOptions, "body">) => request<T>("GET", options),

post: <T>(options: RequestOptions) => request<T>("POST", options),
put: <T>(options: RequestOptions) => request<T>("PUT", options),
delete: <T>(options: RequestOptions) => request<T>("DELETE", options),
patch: <T>(options: RequestOptions) => request<T>("PATCH", options),
};
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
"prepare": "husky"
},
"dependencies": {
"@tanstack/react-query": "^5.90.20",
"@tanstack/react-query-devtools": "^5.91.2",
"axios": "^1.13.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.562.0",
Expand Down
Loading