이 시리즈는 증시 자동화 구축기 8편의 후속입니다.
이전 글: 운영기 B-3 — 외부 API 안정화: RetryUtil 도입과 모델 폐기 대응
다음 글: 운영기 C-2 — 운영 이슈 15건: 실제로 무엇이 터졌고 어떻게 고쳤나
구축기에서 프론트엔드는 거의 다루지 않았다
구축기 1편에서 "Vue 3 + Vuetify로 대시보드를 만들었다"고 한 줄 정리하고 넘어갔다. 백엔드와 파이프라인이 이 시스템의 핵심이고, 프론트엔드는 그 결과를 보는 창이었다.
지금은 다르다. 기능이 추가될 때마다 화면이 따라서 늘었고, 현재는 12개 화면과 13개 API 모듈로 성장했다. 이 편은 그 성장 과정과 설계 결정들을 정리한다.
현재 화면 구성
| 화면 | 경로 | 핵심 기능 |
|---|---|---|
| 로그인 | /login |
JWT 발급, localStorage 저장 |
| 대시보드 | / |
시장 지수 카드, 포트폴리오 요약, 최근 블로그 |
| 시장 현황 | /market |
지수+환율 표, 30일 추이 차트 |
| 포트폴리오 | /portfolio |
보유 종목 표, AI 비중 제안 배너 |
| 배당금 | /dividend |
배당 수령 이력 |
| 뉴스 | /news |
수집 뉴스 목록, 감성 분류 필터 |
| 테마 관리 | /theme |
키워드 PENDING 승인·거절, CRUD |
| AI 분석 | /analysis |
BUY/HOLD/SELL 판단 결과 |
| 블로그 | /blog |
7탭 (일간·주간·월간·실적·경제지표·ETF배당·주간이슈) |
| 실적 캘린더 | /earnings |
달력 통합 뷰, 실적·경제지표·배당 배지 |
| 매매 내역 | /trade |
매매 CRUD, CSV 업로드 |
| 설정 | /settings |
파이프라인 수동 실행, ETF 관리 |
처음에는 대시보드, 뉴스, 분석, 블로그, 매매 내역 5개였다. 기능이 추가될 때마다 화면이 생겼고, 기존 화면이 확장됐다. BlogView는 처음에 탭이 3개였는데 지금은 7개다.
프로젝트 구조
stock-account-gui2/src/
├── api/ # 도메인별 API 모듈 (13개)
│ ├── axios.js # 공통 인스턴스 + 인터셉터
│ ├── market.js
│ ├── trade.js
│ ├── news.js
│ ├── portfolio.js
│ ├── dividend.js
│ ├── blog.js
│ ├── earnings.js
│ ├── economic.js
│ ├── etf.js
│ ├── cash.js
│ ├── issue.js
│ └── theme.js
├── components/layout/
│ ├── AppHeader.vue
│ └── AppSidebar.vue
├── router/index.js # 라우트 정의 + 인증 가드
├── stores/counter.js # Pinia (최소 사용)
└── views/ # 12개 화면 컴포넌트
[이미지 1 — 현재 라우트 구조 + 12개 화면 사이드바 맵]
API 모듈 분리 — View에서 URL을 직접 쓰지 않는 이유
초기에는 View 컴포넌트에서 axios를 직접 호출했다.
// 초기 방식 — View 컴포넌트에서 URL 직접 사용
const response = await axios.get('/api/blog/drafts?date=' + today)
화면이 늘면서 문제가 드러났다. 같은 URL이 여러 곳에 흩어지고, 백엔드 경로가 바뀌면 뷰 파일을 하나씩 뒤져야 했다.
도메인별로 API 모듈을 분리했다.
// api/blog.js
export const getBlogDrafts = (date) =>
axiosInstance.get(`/api/blog/drafts`, { params: { date } })
export const getWeeklyDrafts = (weekDate) =>
axiosInstance.get(`/api/blog/weekly`, { params: { weekDate } })
export const getMonthlyDrafts = (month) =>
axiosInstance.get(`/api/blog/monthly`, { params: { month } })
// BlogView.vue — URL을 모른다
import { getBlogDrafts } from '@/api/blog'
const drafts = await getBlogDrafts(today)
백엔드 경로가 바뀌면 api/blog.js 한 파일만 수정한다. View는 건드리지 않는다. 13개 모듈이 13개 도메인의 API를 각각 담당한다.
Axios 인터셉터 — 요청과 응답을 한 곳에서 처리
모든 API 모듈이 공유하는 단일 Axios 인스턴스(api/axios.js)에 두 가지 인터셉터가 붙는다.
요청 인터셉터: 모든 요청에 Authorization: Bearer {token} 헤더를 자동으로 붙인다.
axiosInstance.interceptors.request.use(config => {
const token = localStorage.getItem('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
응답 인터셉터: HTTP 401 응답 시 토큰을 제거하고 로그인 페이지로 강제 이동한다.
axiosInstance.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('accessToken')
router.push('/login')
}
return Promise.reject(error)
}
)
View 컴포넌트에서 토큰 처리를 신경 쓸 필요가 없다. 인터셉터가 전역에서 처리하기 때문이다.
[이미지 2 — Axios 인터셉터 요청/응답 흐름도]
Refresh Token 자동 갱신이 없는 이유
서버 DB에는 Refresh Token이 저장되어 있다(tb_refresh_token). /api/auth/refresh 엔드포인트도 존재한다. 그런데 프론트엔드에서 자동 갱신 흐름을 구현하지 않았다.
이유는 단순하다. 개인 투자 시스템이고, 주로 아침에 한 번 열어서 확인하는 용도다. 1시간 세션이 만료되면 로그인 화면이 나오고, 다시 로그인하면 된다. 하루에 한두 번 발생하는 불편이고, 구현 복잡도에 비해 효용이 낮다고 판단했다.
자동 갱신을 구현하려면 응답 인터셉터에서 이렇게 처리해야 한다.
// 미구현 상태 — 추후 추가 가능한 패턴
axiosInstance.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401 && !error.config._retry) {
error.config._retry = true
try {
const refreshToken = localStorage.getItem('refreshToken')
const res = await axios.post('/api/auth/refresh', { refreshToken })
localStorage.setItem('accessToken', res.data.accessToken)
error.config.headers.Authorization = `Bearer ${res.data.accessToken}`
return axiosInstance(error.config) // 원래 요청 재시도
} catch (e) {
localStorage.removeItem('accessToken')
router.push('/login')
}
}
return Promise.reject(error)
}
)
_retry 플래그로 무한 루프를 막는 것이 핵심이다. Refresh Token마저 만료됐을 때 다시 401이 오면 그대로 로그인 페이지로 보낸다.
BlogView — 탭이 3개에서 7개로 늘어난 과정
BlogView는 시스템에서 탭이 가장 많이 늘어난 화면이다.
| 탭 | 추가 시점 | 연결 API |
|---|---|---|
| 일간 블로그 | 초기 | blog.js |
| 주간 블로그 | Phase 초기 | blog.js |
| 월간 블로그 | Phase 5 | blog.js |
| 실적 분석 | Phase 7 | earnings.js |
| 경제지표 분석 | Phase 11 | economic.js |
| ETF 배당 분석 | Phase 4 | etf.js |
| 주간 이슈 | Phase 10 | issue.js |
기능이 추가될 때마다 탭이 하나씩 붙었다. 지금은 7탭이지만, 화면 자체는 BlogView.vue 하나다. 탭 컴포넌트 안에서 각 탭이 해당 API 모듈을 부른다.
주간 블로그 탭을 추가할 때 :key 바인딩 오류가 발생했다. (ISSUE-003)
// 문제 코드 — 존재하지 않는 프로퍼티
<v-list-item v-for="item in weeklyDrafts" :key="item.weeklyDraftNo">
// 수정 코드 — 실제 프로퍼티명
<v-list-item v-for="item in weeklyDrafts" :key="item.weeklyNo">
백엔드 응답 필드명을 확인하지 않고 임의로 추측해서 발생한 실수다. Vue는 key가 undefined여도 경고만 주고 렌더링은 된다. 목록이 불안정하게 동작하는 증상으로 나타났다.
운영에서 발견한 프론트엔드 버그들
ISSUE-002: 파이프라인 수동 실행 순서 오류
SettingsView에서 파이프라인 수동 실행 버튼 순서가 실제 스케줄과 반대였다. "블로그 생성(09:10) → 메일 발송(09:00)" 순으로 표시됐는데 실제 실행 순서는 반대다. Notion 동기화 버튼 자체가 누락되어 있었다.
초기 구현 시 스케줄 순서를 확인하지 않고 임의 배치한 결과다. 운영하면서 설정 화면을 열었다가 발견했다.
ISSUE-006: Enter + blur 이중 호출
PortfolioView에서 현금 잔액 입력 필드에 값을 입력하고 Enter를 누르면 saveCash()가 두 번 호출됐다.
// 원인 흐름
@keydown.enter → saveCash() 실행 → editingCash.value = null
→ input이 DOM에서 제거됨 → @blur 이벤트 발생 → saveCash() 한 번 더
// 해결: 최상단 가드
const saveCash = async () => {
if (!editingCash.value) return // 이중 호출 방지
...
}
DOM 이벤트 발생 순서를 고려하지 않은 설계였다. @keydown.enter와 @blur가 연속으로 발생하는 상황을 처음에 고려하지 못했다.
[이미지 3 — BlogView 7탭 구조와 API 모듈 연결]
Pinia는 왜 거의 쓰지 않는가
Pinia가 등록되어 있지만 실제 사용은 최소화했다. 대부분의 상태는 각 View 컴포넌트의 ref에서 로컬로 관리한다. 이유는 단순하다.
화면 간 공유 상태가 거의 없다. 뉴스 목록은 NewsView에서만 쓰이고, 포트폴리오 데이터는 PortfolioView에서만 쓰인다. 서로 다른 화면이 같은 데이터를 참조할 필요가 없으니 전역 상태가 필요하지 않다.
인증 토큰만 예외다. localStorage에 저장해서 새로고침 후에도 유지된다. 이것도 Pinia가 아닌 localStorage를 직접 사용한다. Pinia store가 새로고침 시 초기화되기 때문이다.
현재 설계의 한계
공유 컴포넌트가 없다
View마다 직접 API를 호출하고, 비슷한 데이터 패칭 로직이 여러 곳에 반복된다. 화면이 12개가 됐으니 이제 Composable로 공통 로직을 추출하는 것이 적합한 시점이 됐다. 예를 들어 날짜 선택 + API 호출 + 로딩 상태 관리는 여러 View에서 반복된다.
VITE_API_BASE_URL 관리
API 서버 주소가 빌드 시 번들에 정적으로 포함된다. 서버 IP가 바뀌면 재빌드가 필요하다. CI/CD를 도입한다면 환경 변수 주입 방식을 정립해야 한다.
Refresh Token 자동 갱신 미구현
운영 중 가장 자주 불편한 부분이다. 아침에 대시보드를 열어두고 점심에 다시 보면 로그인 화면이 나온다. 구현 패턴은 이미 정리됐으니 언제든 추가할 수 있다.
정리
| 항목 | 현황 | 비고 |
|---|---|---|
| 화면 수 | 12개 | Phase 추가마다 1~2개씩 증가 |
| API 모듈 | 13개 | 도메인별 1:1 분리 |
| 공유 컴포넌트 | 2개 (Header·Sidebar) | Composable 추출 고려 시점 |
| 상태 관리 | localStorage + 로컬 ref | Pinia는 최소 사용 |
| 토큰 갱신 | 401 시 재로그인 | Refresh Token 자동갱신 미구현 |
다음 편에서는 운영 중 실제로 터진 이슈 15건을 정리한다. 버그·스키마 오류·UI 실수·외부 API 변경·설계 개선까지 유형별로 분류해서 기록한다.
'개발 기록 > 미국 증시 분석 자동화 시스템 운영기' 카테고리의 다른 글
| [운영기 C-3] 백엔드 아키텍처 현재 — 구축기 이후 도메인 19개, Phase 11개 완료 (0) | 2026.06.08 |
|---|---|
| [운영기 C-2] 운영 이슈 15건 — 실제로 무엇이 터졌고 어떻게 고쳤나 (0) | 2026.06.07 |
| [운영기 B-2] ETF 배당 자동화 — 종목 유형별 GPT 프롬프트 전략과 코드 변경 없는 신규 ETF 추가 (0) | 2026.05.30 |
| [운영기 B-1] 실적·경제지표 캘린더 구현 — 예정값과 실제값이 시간차로 들어올 때 DB 설계 (0) | 2026.05.30 |
| [운영기 A-3] 주간·월간·분기 파이프라인 설계 — 7일·30일·90일 데이터를 Claude에 넘기는 방법 (1) | 2026.05.22 |