프론트엔드 개발자로서 협업을 하다보면 겪는 현상인 "중구난방"
API 통신 관련 로직 또한 협업을 하다보면 중구난방이 됩니다.
특히 api를 기능별로 나눈다면, 예를들어 AuthApi (로그인 및 회원가입 등), FriendApi(친구추가 및 삭제 기능등)으로 역할을 분배했을때, 개발자별로 스타일이 다르기때문에 매개변수가 다를수도 있고, 이 함수를 쓰는 핸들러의 위치와 로직이 달라질 가능성이 많습니다.
이를 명확하게 하기 위해선 어떤것의 최선의 방식인지 동료와 상의를 해야하고 설득을 해야합니다.
설득을 해야할 당신을 위해 이 글을 바칩니다.
1. 문제 상황 및 개요
✅ 기존 문제점:
- API 호출 로직이 중구난방으로 섞여 있어 유지보수가 어려움
- axios 요청이 여러 곳에서 직접 사용됨 → 중복 코드 증가 (이 말은 가능하다 중복코드를 줄인다면 성능개선을 할수도 있다는 의미)
- API 요청 시 헤더 설정이 일관되지 않음 -> 동료 개발자를 설득해야하는 포인트
- 데이터 가공, API 호출, 상태 업데이트가 하나의 함수에서 처리됨 → 가독성이 떨어짐 (* 핸들러에 책임을 분배해야함)
🎯 목표: API 통신 로직 삼권분립(헤더, 바디 데이터 전송, 바디 데이터 구성(제작) )
- API 요청을 담당하는 apiClient.js에서 헤더 설정을 통합 관리
- API 함수 (receiptApi.js)는 서버와 통신하는 역할만 수행
- UI에서 사용하는 핸들러 (ReceiptModal.js)는 데이터 가공 및 처리만 담당
2. 역할 분리 및 구조 개선
(1) apiClient.jsx - API 요청을 담당하는 중앙 허브 (Header 담당)
API 통신 로직의 중복을 줄여주는 중요한 인스턴스입니다.
1. 서버에서 요청한 토큰들을 헤더에 담아 자동으로 추가
-> 다른 API 함수에서는 굳이 직접 추가할 필요가 없이 이 axiosInstance를 활용하면 되므로 코드수를 엄청나게 줄입니다.
2. 요청 인터셉터 (요청하기전의 전처리 과정)
여기서 1번에 서술한 내용이 들어가며, axios 인스턴스 객체는 기본 content type을 json으로 해놨지만 가끔 multiFormData를 보내야할 경우가 존재할때는 요청 인터셉터의 config.data( 보낼 데이터의 타입)을 검증하는 로직을 추가합니다
3. 응답 인터셉터 (응답 받기전의 전처리 과정)
여기서는 발생할수 있는 에러들을 처리합니다. 치명적인 에러가 뜨기전에 방어막의 역할.
import axios from 'axios';
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_URL, // 환경 변수에서 API URL 설정
headers: {
'Content-Type': 'application/json',
},
});
// 요청 인터셉터: 모든 요청에 인증 토큰을 자동 추가
apiClient.interceptors.request.use(
(config) => {
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
if (accessToken) {
config.headers['ACCESS-AUTH-KEY'] = `BEARER ${accessToken}`;
}
if (refreshToken) {
config.headers['REFRESH-AUTH-KEY'] = `BEARER ${refreshToken}`;
}
if (config.data instanceof FormData) {
delete config.headers['Content-Type']; // axios가 자동 설정
}
return config;
},
(error) => Promise.reject(error)
);
// 응답 인터셉터: 401 에러 발생 시 자동 로그아웃 처리
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response && error.response.status === 401) {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(error);
}
return Promise.reject(error);
}
);
export default apiClient;
✅ 정리하면
- 모든 API 요청을 apiClient로 통일
- 헤더 설정을 자동화하여 일관된 인증 방식 유지
- 401 Unauthorized 발생 시 자동 로그아웃 처리 (디버깅 시에는 귀찮아질 수 있지만 보안상으로는 좋습니다.)
(2) receiptApi.js - 서버와 통신하는 API 함수 모음
사실 이 API 함수는 수많은 API 함수 중 하나를 예시로 들기 위해 갖고온겁니다. ㅎ
각설하고 설명하면
import apiClient from './apiClient';
// ✅ 서버에 영수증 업로드 요청 (멀티파트 폼 데이터 사용)
export const uploadReceiptImage = async (receiptData) => {
try {
console.log(`🟢 [uploadReceiptImage] 요청: ${receiptData.date}, ${receiptData.category}`);
const formData = new FormData();
formData.append('file', receiptData.image);
// JSON 데이터를 File 객체로 변환하여 추가
const json = JSON.stringify({
date: receiptData.date,
category: receiptData.category,
description: receiptData.description,
});
const jsonFile = new File([json], 'info.json', { type: 'application/json' });
formData.append('info', jsonFile);
// API 요청 보내기
const response = await apiClient.post('/expense/receipt', formData);
console.log(`✅ [uploadReceiptImage] 업로드 성공! 응답:`, response.data);
return response.data;
} catch (error) {
console.error(`❌ [uploadReceiptImage] 업로드 실패!`, error.response?.data || error.message);
throw error;
}
};
// ✅ 특정 연도/월의 영수증 조회
export const fetchReceipt = async (year, month) => {
try {
const response = await apiClient.get('/expense/receipt', { params: { year, month } });
console.log(response.data);
return response.data;
} catch (error) {
console.error('❌ 영수증 조회 오류:', error);
throw error;
}
};
✅ 역할:
- API 요청을 보내는 역할만 수행 (데이터 가공 X, 상태 변경 X)
- 서버와의 명확한 인터페이스 제공
- 매개변수 하나만 받도록 통일하여 일관성 유지
(3) ReceiptModal.js - UI 핸들링 & 서버에 전송할 데이터 가공 담당
리액트를 처음 배웠을때 배우는 상태를 활용한 UI/UX 전달법을 활용해서 설명해보겠다.
1. 상태 정의 -> 여기서 서버에 우리가 보내야할 정보들의 기본틀을 설정한다
서버에서 요구한 명세서는 다음과 같다. 먼저 해당 모든 JSON 객체의 타입은 multiForm 타입이어야했다.
또한 서버에서 요청한 데이터의 구조는 file 속성 그리고 info 객체 속성이 있고 그 내부 info 객체의 속성값으로 date, category, description이라는 내용이 포함을 필수적으로 보내야했다.
따라서 삼권분립 원칙에 따라 우리가 보낼 데이터 상태와 서버의 요구사항을 정확히 맞춰줘야한다.
- 먼저 보내야할 상태의 타입은 객체의 형태니, 우리는 useState를 활용하여 상태를 초기화할때 객체 형태로 초기화한다.
const [receiptData, setReceiptData] = useState({
image : null,
category :'',
description :'',
date : date // 해당 date는 useParams라는 매개변수로 따로 빼두었고 이를 그대로 서버에 전송하면 된다.
});
- 지금까지 상황을 정리하면, 컴포넌트에서 서버에서 보내야할 데이터는 receiptData 객체이며 date 속성은 우리가 params로 따로 설정해두었기에, image, category, description 이 3가지의 속성을 사용자로부터 받아와서 값을 저장한 후. 서버에 전달만 해주면 이 컴포넌트의 역할은 다한것이다.
사용자로 부터 값을 어떻게 받을까? -> 바로 이벤트 핸들러로 받는다.
2. 핸들러 정의
위에서 설명했듯 우리는 3가지의 속성에 대한 값을 사용자로부터 받아야한다 그럼 어떻게 할까?
사용자가 어떤 input 태그의 해당하는 UI에 값을 집어넣었을때 우린 그 변화를 handle해야한다.
따라서 handleChange를 할 함수를 생성해야햔다. -> 3개
만들면 되겠다. 불변성 준수해서 prev 써서 3개만들면
// 이미지 선택 핸들러 (receiptData 상태 업데이트)
const handleImageChange = (e) => {
const file = e.target.files[0];
if (file && (file.type === 'image/jpeg' || file.type === 'image/png')) {
setReceiptData((prev) => ({ ...prev, image: file }));
} else {
alert('JPG 또는 PNG 형식의 이미지를 업로드해 주세요.');
}
};
// ✅ 카테고리 선택 핸들러
const handleCategoryChange = (e) => {
setReceiptData((prev) => ({ ...prev, category: e.target.value }));
};
// ✅ 메모 입력 핸들러
const handleDescriptionChange = (e) => {
setReceiptData((prev) => ({ ...prev, description: e.target.value }));
};
//3개를 잘 받았으면 .. 3개를 총망라해서 보내는 함수 구현 -> api 함수느 우리가 잘 짜놨죠!!
// 영수증 업로드 핸들러
const handleUpload = async () => {
if (!receiptData.image) {
alert('이미지를 먼저 선택하세요.');
return;
}
if (!receiptData.category) {
alert('카테고리를 선택하세요.');
return;
}
setLoading(true);
try {
await uploadReceiptImage(receiptData); // ✅ receiptData 객체 하나로 전달
alert('영수증이 성공적으로 업로드되었습니다!');
navigate('/main');
} catch (error) {
console.error('영수증 업로드 실패:', error);
alert('업로드 중 오류가 발생했습니다.');
} finally {
setLoading(false);
}
};
3. return 내부 (UI) 정의 -> 사용자가 입력할 공간 창출
return (
<>
{isOpen && (
<div className="modal-overlay">
<div className="modal-container">
<h2>{date} 영수증 추가</h2>
<div className="entry-form">
{/* 이미지 업로드 */}
<input
type="file"
accept="image/jpeg, image/png"
onChange={handleImageChange}
/>
{receiptData.image && (
<div className="preview-section">
<img
src={URL.createObjectURL(receiptData.image)}
alt="영수증 미리보기"
/>
</div>
)}
{/* 카테고리 선택 */}
<select
value={receiptData.category}
style={{ marginBottom: '20px' }}
onChange={handleCategoryChange}
>
<option value="">카테고리를 선택하세요</option>
<option value="식비">🍽️ 식비</option>
<option value="교통비">🚗 교통비</option>
<option value="쇼핑">🛍️ 쇼핑</option>
<option value="주거비">🏠 주거비</option>
<option value="취미/여가">🎨 취미/여가</option>
<option value="기타">✏️ 기타</option>
</select>
{/* 메모 입력 */}
<input
type="text"
placeholder="메모를 추가하세요"
value={receiptData.description}
onChange={handleDescriptionChange}
/>
{/* 버튼 */}
<button
className="save-button"
onClick={handleUpload}
disabled={loading}
>
{loading ? '업로드 중...' : '영수증 업로드'}
</button>
<button className="close-button" onClick={onClose}>
취소
</button>
</div>
</div>
</div>
)}
</>
);
};
export default ReceiptModal;
✅ 역할:
- UI에서 사용자 입력을 받아 데이터 객체 생성
- API 함수를 호출하여 서버에 데이터 전송
- API 호출에 대한 응답을 사용자에게 피드백
🎯 최종 정리: 역할 분리를 통한 코드 개선 : 삼권분립!
✔ apiClient.jsx: API 요청 및 헤더 설정 관리
✔ receiptApi.jsx: 서버와 통신하는 API 함수 제공
✔ ReceiptModal.jsx: UI에서 데이터를 가공하고 API 호출
'FrontEnd Develop > Project : Wallet Guardians' 카테고리의 다른 글
소극적인 재무관리를 하는 20대를 위한 프로젝트, <Wallet Guardians> #1. 시작했던 계기와 기능소개 (0) | 2025.02.21 |
---|---|
프로젝트 Wallet Guardians의 프론트 개발자로서 했던 몇가지 고민 Part #1. (0) | 2025.02.17 |
본격 연동 #7. API 함수에서의 능동적인 에러 처리 (409, 404) (0) | 2025.02.11 |
본격 연동 #6. 비동기 데이터 로딩 시 UI 깜빡임 방지 (await & setTimeout 활용)으로 사용자 경험 증대 (0) | 2025.02.11 |
본격 연동 #5. 비동기 처리의 이해로 로그인 후 리다이렉션 처리 (0) | 2025.02.10 |