Axios Interceptors로 내가 필요한 부분을 잘 반영할 수 있었고 성능의 향상도 이끌었다. 내가 느낀 장단점을 알려주고 싶었고 가상 페이지를 예시로 인터셉터 기능을 설명해볼까한다.
AXIOS
Axios는 node.js와 브라우저를 위한 Promise 기반 HTTP 클라이언트 라이브러리입니다. 그것은 동형 입니다(동일한 코드베이스로 브라우저와 node.js에서 실행할 수 있습니다). 서버 사이드에서는 네이티브 node.js의 http 모듈을 사용하고, 클라이언트(브라우저)에서는 XMLHttpRequests를 사용합니다.
동형 애플리케이션 isomorphic application
동형성(isomorphic). 즉, 동일한 형태. 웹 개발에서 동형 애플리케이션은 코드가 서버와 클라이언트 사이드에서 동일한 코드 베이스를 공유할 수 있는 애플리케이션이다. 빠르고 SEO 친화적이며 코드를 적게 작성할 수 있도록 도와준다는 장점이 특징이다.
왜 사용해야 할까?
AXIOS는 주로 웹브라우저와 Node.js 환경에서 사용된다. HTTP 요청을 쉽게 보낼 수 있도록 도와주어 JavaScript에서 인기 있는 HTTP 클라이언트 라이브러리이다.
✔ Promise 기반(ES6) : 비동기 코드를 간결하고 직관적으로 작성할 수 있도록 Promise 사용
✔ 브라우저와 Node.js 지원 : 동일한 코드 베이스로 클라이언트 사이드와 서버 사이드 양쪽에서 HTTP 요청 처리
✔ Interceptor : 요청을 보내기 전이나 서버로부터 응답을 받기 전에 데이터를 변형할 수 있는 기능 제공
✔ 요청 취소 : 이미 보낸 요청을 취소할 수 있는 기능 제공
✔ 자동 Json 변환 : 서버로부터 받은 데이터를 자동으로 JOSN 형태로 변환
✔ 클라이언트 지원 : XSRF 방지를 위한 CSRF 토큰과 같은 보안 기능을 내장
나는 Axios의 여러 기능 중에서 Interceptors를 사용해보았다.
AXIOS Interceptors
then 또는 catch로 처리되기 전에 요청과 응답을 가로채는 Axios의 기능.
How Axios interceptor works
- Request: 요청이 서버로 전송되기 전에 Request Interceptor가 Trigger된다. ⇒ Request Header를 수정하거나 Token을 첨부하거나 요청이 전송되기 전에 필요한 전처리를 수행
- Response: 서버로부터 Response가 수신되면 Response Interceptor가 Trigger된다. 오류 처리를 하거나 Response가 전달되기 전에 필요한 데이터 변환 등을 수행한다.
Interceptor는 여러 요청에 걸쳐 공통 기능을 중앙 집중화하는 방법을 제공한다. 애플리케이션에서 공통된 요청이나 응답 프로세스를 효율적으로 처리하고 보안을 강화하며 API 인터렉션을 간소화할 수 있다.
예를 들어 모든 요청에 Authorization token을 포함해야하는 경우 헤더에 자동으로 추가를 시켜주는 Request Interceptor를 정의하기만 하면된다..! 개별적으로 수동적으로 추가할 필요가 없어지는 것.
// Axios 공식 Docs에서의 예시
// 요청 인터셉터 추가하기
axios.interceptors.request.use(function (config) {
// 요청이 전달되기 전에 작업 수행
return config;
}, function (error) {
// 요청 오류가 있는 작업 수행
return Promise.reject(error);
});
// 응답 인터셉터 추가하기
axios.interceptors.response.use(function (response) {
// 2xx 범위에 있는 상태 코드는 이 함수를 트리거 합니다.
// 응답 데이터가 있는 작업 수행
return response;
}, function (error) {
// 2xx 외의 범위에 있는 상태 코드는 이 함수를 트리거 합니다.
// 응답 오류가 있는 작업 수행
return Promise.reject(error);
});
request나 response에 대해 모든 인터셉터를 지울 수도 있다.
// 인터셉터 제거하기
const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);
// 모든 인터셉터 제거하기
const instance = axios.create();
instance.interceptors.request.use(function () {/*...*/});
instance.interceptors.request.clear(); // Removes interceptors from requests
instance.interceptors.response.use(function () {/*...*/});
instance.interceptors.response.clear(); // Removes interceptors from responses
그리고 내가 원하는 Axios instance에만 인터셉트를 추가할 수도 있다. 보통 Axios intance를 전역으로 쓰는 경우가 많은데, 커스텀으로 인스턴스를 따로 만들어 부분적으로 인터셉트할 수 있다.
// 커스텀 axios instance에 interceptor 따로 추가하기
const instance = axios.create();
instance.interceptors.request.use(function () {/*...*/});
이 외에 Request Interceptor의 경우엔 동기적으로 우선 처리될 수 있게 요청하는 기능과 특정 시간에 실행해야하는 요청을 설정할 수 있고, 다중 인터셉터 기능 등도 지원한다. npm의 axios 패키지 내용에 아주 상세히 나와있다.
My Case
응답을 받은 큰 JSON 데이터를 용도에 맞게 분류하고 싶었다. 분류된 데이터들 중 특정 컴포넌트에 필요한 데이터만 각각 로드하여 애플리케이션 반응속도를 개선하고 싶었다. 서버에서 분류하여 전달해주면 정말 좋겠지만,,
Axios의 중앙 집중식 데이터 처리로, 특정 페이지에서 받는 데이터를 한꺼번에 변환 및 분류할 수 있고 검증 로직을 설정할 수 있는 interceptor를 사용하게 되었다.
예를 들어 마이페이지를 접속했을 때 제품 정보는 제품 목록으로, 사용자 관련 정보는 항상 들고있는 헤더에 띄우고 싶을 수 있겠다.
📌 추가적으로 가져온 데이터를 기반으로 복잡한 UI를 그리는 과정에서 블로킹 문제가 있을 수 있다. 스켈레톤 UI나 특정 이벤트시 받아올 수 있게 처리해도 좋을 것 같다. 데이터를 받아오지 못한 케이스도 생각해봐야 하겠다,,!
구현 : Axios Interceptors를 적용해보자
우선 Axios가 설치되어 있음을 가정하고 시작하겠다. 각 환경에 맞게 install axios를 해주면 된다.
Create Axios instance and configurations
프로젝트의 src 폴더에 AxiosInstance.tsx 파일을 생성해본다. Axios를 사용해서 실제 HTTP 요청을 하는 function이 포함된다.
interceptor를 사용할 코드를 만들어보자,, User와 Product Data가 한꺼번에 오고있다고 가정한다.
- User님 환영합니다!
- 구매한 상품을 표시해야된다.
적절한지는 모르겠는데 데이터를 분류하는 방식을 보여주기 위해 더미데이터를 받아오겠다!
myPageAxiosInstance.ts
주된 목적은 전체 프로젝트에서 애플리케이션에서 HTTP 요청을 보낼 때 공통적으로 사용되는 설정을 사전에 정의하는 곳이다.
axios.create()를 사용해서 모든 API의 엔드포인트에 대한 baseurl을 설정해주었다. 그리고 Data를 fetch할 때 Interceptor를 사용해보겠다. 보통 AxiosInstance 파일은 공통으로 사용되지만 나는 특정 페이지에서 일어날 때 받아오는 데이터를 분류하고 싶었기에 분리를 위해 myPage용으로 만들었다고 가정한다.
import axios from "axios";
import { ExtractedData } from "./fetchData";
const axiosInstance = axios.create({
baseURL: "https://dummyjson.com/products",
});
axiosInstance.interceptors.response.use(
(response) => {
const { id, ...productData } = response.data;
const extractedData: ExtractedData = {
userData: { id },
otherData: productData,
};
response.data = extractedData;
return response;
},
(error) => {
return Promise.reject(error);
}
);
export default axiosInstance;
fetchData.ts
myPageAxiosInstance를 이용하여 유저 정보를 fetch 받는다. (더미 데이터로 랜덤 테스트)
import axiosInstance from "./axiosInstance";
export interface UserInfo {
id: number;
}
export interface ProductInfo {
title: string;
description: string;
price: number;
discountPercentage: number;
rating: number;
stock: number;
brand: string;
category: string;
thumbnail: string;
images: string[];
}
export interface ExtractedData {
userData: UserInfo;
otherData: ProductInfo;
}
export const fetchData = async (): Promise<ExtractedData> => {
const randomId = Math.floor(Math.random() * 10) + 1;
return axiosInstance
.get(`/${randomId}`)
.then((response) => {
return response.data;
})
.catch((error) => {
console.error("Error fetching data:", error);
throw error;
});
};
Login.tsx
유저와 관련된 Data를 받을 Login 컴포넌트
import { UserInfo } from "./fetchData";
interface LoginProps {
userData: UserInfo;
}
const Login = ({ userData }: LoginProps) => (
<div>{userData.id}고객님, 환영합니다!</div>
);
export default Login;
Product.tsx
제품과 관련된 Data를 받을 Product 컴포넌트
import { ProductInfo } from "./fetchData";
interface ProductProps {
otherData: ProductInfo;
}
const Product = ({ otherData }: ProductProps) => (
<div>
<h2>{otherData.title}</h2>
<img
src={otherData.thumbnail}
alt={otherData.title}
style={{ width: "100px", height: "100px" }}
/>
<p>{otherData.description}</p>
<p>
Price: ${otherData.price} (Discount: {otherData.discountPercentage}%)
</p>
<p>
Rating: {otherData.rating} (Stock: {otherData.stock})
</p>
<p>Brand: {otherData.brand}</p>
<p>Category: {otherData.category}</p>
<div>
{otherData.images?.map((image, index) => (
<img
key={image}
src={image}
alt={`Product Image ${index + 1}`}
style={{ width: "50px", height: "50px", marginRight: "10px" }}
/>
))}
</div>
</div>
);
export default Product;
App.tsx
import { useEffect, useState } from "react";
import { fetchData, UserInfo, ProductInfo } from "./fetchData";
import Login from "./Login";
import Product from "./Product";
import ProductSkeleton from "./ProductSkeleton";
const App = () => {
const [userData, setUserData] = useState<UserInfo | null>(null);
const [otherData, setOtherData] = useState<ProductInfo | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadProductData = async () => {
try {
setLoading(true);
const { userData, otherData } = await fetchData();
setUserData(userData);
setOtherData(otherData);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setLoading(false);
}
};
loadProductData();
}, []);
return (
<div>
{loading ? (
<ProductSkeleton />
) : (
<>
{userData && <Login userData={userData} />}
{otherData && <Product otherData={otherData} />}
</>
)}
</div>
);
};
export default App;
ProductSkeleton은 로딩 일어날 때를 대비해서 만들어져 있다고 가정한다.
userData와 otherData를 원하는 컴포넌트에 전달해준다
🤔💭
Axios Interceptors를 활용하여 API 데이터를 분류하는 방법을 적어보았다. 난 이 방법으로 프로젝트의 데이터 처리 과정의 효율성을 눈에 띄게 높였다. 그러나 내가 실제로 적용한 케이스의 코드를 보여주지 않고 예시로 만든 코드이고 내가 효과를 본 부분에 대해서 정확한 전달이 어려움을 크게 느꼈다.. 🥹 이 또한 부족함이라 생각한다. 내가 생각지 못한 부분도 있었는데, 생각보다 더욱 큰 데이터를 받아오고 UI를 그려야할 경우 블로킹이 생길 수 있고 외의 성능 저하, 타입 안정성 문제, 예외 처리 등의 문제가 생길 수도 있다. 케이스 바이 케이스로 신중한 고려가 필요하기에 Axios Interceptors를 사용할 때는 충분히 인지하고, 프로젝트의 목적과 요구 사항에 따라 내 글을 통해 고려해볼 수 있었으면 한다.
[What is an isomorphic application?]
[Axios Interceptors in a React application]
ChatGPT 🤖
'Problem Drilling' 카테고리의 다른 글
node.js express/multer 사용시 UTF-8 filename 미지원 이슈 해결 과정 (7) | 2024.10.13 |
---|---|
브라우저에서 Excel 파일 다운로드하기: Blob과 CORS 해결과정 (0) | 2024.04.22 |
Github Action Test 성공하고 Workflow 복붙했다가 큰 코 다친 이야기 (2) | 2023.11.12 |
Github Actions 사용, Workflow 테스트 자동화 적용하기, pnpm (0) | 2023.11.11 |
Open Source License 고르기, GPL? MIT? No License? (0) | 2023.11.10 |