# v5: 자동 토큰 갱신 및 거래 내역(Deals) 가져오기 이전 단계에서 우리는 수동으로 인증 코드를 받아 액세스 토큰과 **리프레시 토큰**을 성공적으로 발급받았습니다. 이번 포스트에서는 이 리프레시 토큰을 사용하여 액세스 토큰을 자동으로 갱신하는, 완전 자동화의 핵심 로직을 구현합니다. 또한, 자동 갱신된 토큰을 사용하여 Freee API에서 우리의 최종 목표인 **거래 내역(Deals)** 데이터를 가져오는 함수를 만듭니다. ## 1단계: `.env` 파일에 리프레시 토큰 추가 가장 먼저, 이전 단계에서 발급받은 `refresh_token` 값을 `.env` 파일에 추가해야 합니다. ```ini # .env 파일 ... FREEE_CLIENT_SECRET="YOUR_FREEE_CLIENT_SECRET" FREEE_BUSINESS_ID=12261708 FREEE_CALLBACK_URL="urn:ietf:wg:oauth:2.0:oob" FREEE_REFRESH_TOKEN="이곳에_발급받은_리프레시_토큰을_붙여넣으세요" GOOGLE_SERVICE_ACCOUNT_KEY_PATH=... ``` ## 2단계: 토큰 자동 갱신 로직 구현 `axios`의 **인터셉터(Interceptor)** 기능을 사용하면 API 요청과 응답을 전역적으로 가로챌 수 있습니다. 우리는 이 기능을 사용하여, API 요청이 "토큰 만료" 오류(401 Unauthorized)로 실패했을 때, 리프레시 토큰으로 새 토큰을 발급받고 원래 요청을 자동으로 재시도하는 로직을 구현할 것입니다. `src/freeeClient.ts` 파일을 다음과 같이 대대적으로 수정합니다. ```typescript import axios, { AxiosError } from 'axios'; import * as dotenv from 'dotenv'; dotenv.config(); const FREEE_CLIENT_ID = process.env.FREEE_CLIENT_ID; const FREEE_CLIENT_SECRET = process.env.FREEE_CLIENT_SECRET; const FREEE_REFRESH_TOKEN = process.env.FREEE_REFRESH_TOKEN; const FREEE_API_BASE_URL = 'https://api.freee.co.jp'; if (!FREEE_CLIENT_ID || !FREEE_CLIENT_SECRET || !FREEE_REFRESH_TOKEN) { throw new Error('Freee 관련 환경 변수(CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN)가 설정되지 않았습니다.'); } // 액세스 토큰을 메모리에 저장 let accessToken: string | null = null; let isRefreshing = false; let failedQueue: { resolve: (value: unknown) => void; reject: (reason?: any) => void; }[] = []; const processQueue = (error: Error | null, token: string | null = null) => { failedQueue.forEach(prom => { if (error) { prom.reject(error); } else { prom.resolve(token); } }); failedQueue = []; }; const refreshAccessToken = async (): Promise => { try { const response = await axios.post( 'https://accounts.secure.freee.co.jp/public_api/token', new URLSearchParams({ grant_type: 'refresh_token', client_id: FREEE_CLIENT_ID, client_secret: FREEE_CLIENT_SECRET, refresh_token: FREEE_REFRESH_TOKEN, }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } ); const newAccessToken = response.data.access_token; console.log('새로운 액세스 토큰을 발급받았습니다.'); // .env의 REFRESH_TOKEN도 갱신될 수 있으므로, 이를 업데이트하는 로직도 추가하면 좋습니다. // (이번 예제에서는 생략) return newAccessToken; } catch (error) { console.error('리프레시 토큰으로 액세스 토큰을 갱신하는 데 실패했습니다.', error); throw new Error('토큰 갱신 실패. Freee 재인증이 필요할 수 있습니다.'); } }; const freeeApiClient = axios.create({ baseURL: FREEE_API_BASE_URL, headers: { 'Content-Type': 'application/json' }, }); // 요청 인터셉터: 모든 요청에 액세스 토큰을 포함시킴 freeeApiClient.interceptors.request.use( async (config) => { if (!accessToken) { console.log('액세스 토큰이 없습니다. 새로 발급받습니다...'); accessToken = await refreshAccessToken(); } config.headers.Authorization = `Bearer ${accessToken}`; return config; }, (error) => Promise.reject(error) ); // 응답 인터셉터: 401 오류 발생 시 토큰 갱신 및 재시도 freeeApiClient.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config; if (error.response?.status === 401 && originalRequest && !originalRequest._retry) { if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }).then(token => { originalRequest.headers['Authorization'] = `Bearer ${token}`; return freeeApiClient(originalRequest); }); } originalRequest._retry = true; isRefreshing = true; try { const newAccessToken = await refreshAccessToken(); accessToken = newAccessToken; processQueue(null, newAccessToken); originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; return freeeApiClient(originalRequest); } catch (refreshError) { processQueue(refreshError, null); return Promise.reject(refreshError); } finally { isRefreshing = false; } } return Promise.reject(error); } ); ## 3단계: 거래 내역(Deals) 가져오기 함수 작성 이제 자동 갱신 로직이 탑재된 `freeeApiClient`를 사용하여, 특정 기간의 거래 내역을 가져오는 함수를 `src/freeeClient.ts`에 추가합니다. ```typescript // src/freeeClient.ts 파일 하단에 추가 export const getDeals = async (companyId: number, startDate: string, endDate: string) => { try { const response = await freeeApiClient.get('/api/1/deals', { params: { company_id: companyId, start_date: startDate, end_date: endDate, status: 'settled', // 완료된 거래만 limit: 100, // 최대 100개까지 }, }); console.log(`총 ${response.data.deals.length}개의 거래 내역을 가져왔습니다.`); return response.data.deals; } catch (error: any) { if (axios.isAxiosError(error)) { console.error('Freee 거래 내역 조회 실패:', error.response?.data || error.message); } else { console.error('Freee 거래 내역 조회 중 알 수 없는 오류 발생:', error.message); } throw error; } }; ``` ## 4단계: `index.ts`에서 거래 내역 가져오기 테스트 `index.ts`의 내용을 정리하고, `getDeals` 함수를 호출하여 실제 데이터를 가져오는지 테스트합니다. ```typescript import { getDeals } from './freeeClient'; const main = async () => { console.log('프로젝트 시작...'); const companyId = parseInt(process.env.FREEE_BUSINESS_ID || '', 10); if (!companyId) { throw new Error('환경 변수 FREEE_BUSINESS_ID가 올바르게 설정되지 않았습니다.'); } // 예: 2025년 11월 1일부터 11월 30일까지의 거래 내역 조회 const startDate = '2025-11-01'; const endDate = '2025-11-30'; try { const deals = await getDeals(companyId, startDate, endDate); console.log('가져온 거래 내역:', deals); } catch (error) { // 에러는 getDeals 함수에서 이미 처리됨 } console.log('프로젝트 종료.'); }; main().catch(error => { console.error('전체 실행 중 오류 발생:', error); process.exit(1); }); ``` 이제 `.env` 파일에 `FREEE_REFRESH_TOKEN`을 정확히 입력했다면, `npx ts-node src/index.ts`를 실행하여 거래 내역을 성공적으로 가져오는지 확인할 수 있습니다. --- 다음 **v6** 포스트에서는 Freee에서 가져온 거래 내역 데이터를 Vertex AI(Gemini)로 보내 카테고리를 자동 분류하는 방법에 대해 알아보겠습니다.