AI

sQuiry: AI 서버와 통신하는 프론트엔드 개발

블체. 2026. 2. 10. 00:10

서론

이전글: https://egg-stone.tistory.com/43

 

이전 글에서는 sQuiry의 백엔드 구조와 RAG 기반 SQL 생성 엔진에 대해 다뤘다. 

이번 글에서는 프론트엔드 아키텍처와 백엔드와의 통신 구조를 다룬다. 프레임워크 없이 구현한 이유와 전체적인 데이터 플로우를 중심으로 설명한다.

 

기술 스택과 선택 이유

바닐라 JavaScript

React나 Vue 같은 프레임워크를 사용하지 않고 바닐라 JavaScript를 선택했다.

선택 이유:

  • 프로젝트 규모가 작음 (3개 페이지, 독립적인 기능)
  • 빌드 도구 불필요, 즉시 배포 가능
  • 빠른 초기 로딩 속도
  • 복잡한 상태 관리가 필요 없음

프레임워크는 강력하지만 모든 프로젝트에 필요한 것은 아니다. sQuiry는 페이지별로 독립적인 기능을 제공하므로 단순한 구조가 적합했다.

FastAPI + Jinja2

서버 사이드 렌더링을 위해 FastAPI와 Jinja2 템플릿 엔진을 사용했다.

장점:

  • 초기 페이지 로드가 빠름
  • SEO 친화적
  • FastAPI와 자연스럽게 통합

구조:

frontend/
├── routes.py           # 페이지 라우팅
├── templates/          # Jinja2 템플릿
│   ├── base.html      # 공통 레이아웃
│   ├── index.html     # 메인 페이지
│   ├── query.html     # 쿼리 생성 페이지
│   └── history.html   # 히스토리 페이지
└── static/            # 정적 파일
    ├── css/           # 스타일시트
    └── js/            # JavaScript

CSS 변수 기반 스타일링

CSS 변수로 디자인 토큰을 정의하여 일관된 디자인 시스템을 구축했다. 향후 다크 모드나 테마 변경이 쉽도록 설계했다.

 

전체 아키텍처

레이어 구조

사용자
  ↓
[프론트엔드 - HTML/CSS/JS]
  ↓ HTTP Request (JSON)
[FastAPI 라우터]
  ↓
[백엔드 API] (/api/v1/*)
  ↓
[Vanna Engine + DB]
  ↓ HTTP Response (JSON)
[프론트엔드 - JavaScript]
  ↓
DOM 업데이트 → 사용자에게 표시

프론트엔드와 백엔드를 명확하게 분리했다.

  • 프론트엔드 라우트: /, /query, /history → HTML 응답
  • 백엔드 API: /api/v1/* → JSON 응답

이렇게 분리하면 향후 모바일 앱이나 다른 클라이언트에서도 동일한 API를 사용할 수 있다.

페이지 구조

메인 페이지 (/):

  • 서비스 소개
  • 주요 기능 안내
  • 쿼리 생성 페이지로 이동

쿼리 생성 페이지 (/query):

  • 자연어 질문 입력
  • SQL 생성 및 편집
  • SQL 실행 및 결과 표시

히스토리 페이지 (/history):

  • 실행한 쿼리 목록
  • 질문, SQL, 실행 결과 표시

 

백엔드와의 데이터 통신

API 엔드포인트

프론트엔드는 다음 API 엔드포인트와 통신한다.

1. SQL 생성

POST /api/v1/generate-sql
요청: { "question": "지난 달 매출은?" }
응답: { "question": "...", "sql": "SELECT ...", "execution_time": 1.5 }

2. SQL 실행

POST /api/v1/execute-sql
요청: { "sql": "SELECT * FROM orders" }
응답: { "success": true, "data": [...], "row_count": 10 }

3. 히스토리 조회

GET /api/v1/query-history
응답: { "history": [ { "question": "...", "sql": "...", "timestamp": "..." } ] }

데이터 플로우

쿼리 생성 및 실행 플로우:

1. 사용자가 자연어 질문 입력
   ↓
2. JavaScript: POST /api/v1/generate-sql
   ↓
3. 백엔드: Vanna 엔진이 SQL 생성
   ↓
4. 프론트엔드: 생성된 SQL 화면에 표시
   ↓
5. 사용자가 "실행" 버튼 클릭 (또는 SQL 편집 후 실행)
   ↓
6. JavaScript: POST /api/v1/execute-sql
   ↓
7. 백엔드: MariaDB에서 쿼리 실행
   ↓
8. 프론트엔드: 결과를 테이블로 렌더링

JavaScript 구조

코드를 기능별로 모듈화했다.

common.js - 공통 유틸리티

  • API 호출 헬퍼 (fetch 래퍼)
  • 알림 메시지 표시
  • 테이블 생성 함수
  • SQL 포맷팅

query.js - 쿼리 페이지 로직

  • SQL 생성 요청
  • SQL 실행 요청
  • SQL 편집 기능
  • 복사 기능

history.js - 히스토리 페이지 로직

  • 히스토리 로드
  • 히스토리 렌더링

비동기 통신 패턴

모든 API 호출은 async/await로 처리하여 UI 블록을 방지했다.

// API 호출 패턴
async function generateSQL(question) {
    try {
        // 로딩 표시
        showLoading();
        
        // API 호출
        const response = await fetch('/api/v1/generate-sql', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ question })
        });
        
        const data = await response.json();
        
        // 결과 표시
        displaySQL(data.sql);
        
    } catch (error) {
        // 에러 처리
        showError(error.message);
    } finally {
        // 로딩 숨김
        hideLoading();
    }
}

이 패턴을 모든 API 호출에 일관되게 적용했다.

상태 관리

프레임워크를 사용하지 않기 때문에 상태 관리는 단순하게 처리했다.

페이지 수준 상태:

  • 각 페이지는 독립적으로 동작
  • 전역 상태 없음
  • 필요한 데이터는 API에서 다시 가져옴

쿼리 페이지 상태:

let currentSql = '';  // 현재 생성된 SQL

단일 변수로 현재 SQL을 관리한다. 사용자가 편집하면 이 변수를 업데이트하고, 실행 시 이 변수의 값을 API로 전송한다.

히스토리 페이지 상태:

  • 상태를 별도로 유지하지 않음
  • 페이지 로드 시마다 API에서 최신 데이터 가져옴
  • 새로고침 버튼으로 수동 업데이트 가능

 

사용자 경험 설계

단계별 UI 표시

쿼리 생성 페이지는 사용자의 작업 단계에 따라 섹션을 순차적으로 표시한다.

1. 질문 입력 섹션 (항상 표시)
   ↓
2. SQL 생성 클릭
   ↓
3. 생성된 SQL 섹션 표시
   ↓
4-A. 실행 클릭 → 결과 섹션 표시
4-B. 편집 클릭 → 편집 섹션 표시

각 단계에서 사용자는 다음에 무엇을 해야 할지 명확하게 알 수 있다.

피드백 시스템

사용자 행동에 즉각적인 피드백을 제공한다.

로딩 상태:

  • API 호출 중 로딩 스피너 표시
  • 버튼 비활성화로 중복 클릭 방지

성공/실패 알림:

  • 작업 성공 시 녹색 알림 (3초 후 자동 사라짐)
  • 에러 발생 시 빨간색 알림

시각적 피드백:

  • 버튼 호버 시 색상 변경
  • 복사 버튼 클릭 시 체크마크 아이콘으로 변경

에러 처리

모든 API 호출에서 에러를 캐치하고 사용자에게 명확한 메시지를 표시한다.

try {
    const response = await apiCall('/api/v1/generate-sql', ...);
} catch (error) {
    showAlert(error.message, 'error');
}

네트워크 에러, 서버 에러, 검증 에러 모두 일관된 방식으로 처리한다.

 

성능 고려사항

최소한의 JavaScript

JavaScript 파일 크기를 최소화했다.

  • common.js: 약 2KB
  • query.js: 약 5KB
  • history.js: 약 3KB

외부 라이브러리를 사용하지 않아 추가 로딩 시간이 없다.

DOM 조작 최적화

테이블이나 히스토리 목록처럼 여러 요소를 렌더링할 때, HTML 문자열을 한 번에 생성한 후 innerHTML로 삽입한다. 여러 번 DOM을 조작하는 것보다 훨씬 빠르다.

비동기 로딩

페이지 로드와 API 호출을 분리하여 초기 로딩 속도를 개선했다. 히스토리 페이지는 빈 페이지를 먼저 표시하고, JavaScript로 데이터를 비동기 로드한다.

개발 과정의 설계 결정

상태 관리

선택: 페이지별 독립 상태

이유:

  • 페이지 간 공유 상태 없음
  • Redux나 Vuex 같은 상태 관리 라이브러리 불필요
  • 각 페이지는 필요한 데이터를 API에서 직접 가져옴

페이지가 많아지거나 복잡한 상태 공유가 필요하면 재고려할 수 있다.

API 설계

선택: RESTful API

프론트엔드와 백엔드 API를 명확히 분리했다.

  • /api/v1/* - JSON API
  • 그 외 - HTML 페이지

이렇게 분리하면 API 버전 관리가 쉽고, 다른 클라이언트 (모바일 앱 등)에서도 동일한 API를 사용할 수 있다.

배포 구조

FastAPI는 프론트엔드와 백엔드를 하나의 애플리케이션으로 서빙한다.

# main.py
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from frontend.routes import router as frontend_router
from backend.api.routes.query import router as api_router

app = FastAPI()

# API 라우트 등록
app.include_router(api_router)

# 프론트엔드 라우트 등록
app.include_router(frontend_router)

# 정적 파일 서빙
app.mount("/static", StaticFiles(directory="frontend/static"), name="static")

장점:

  • 단일 서버로 배포 가능
  • CORS 문제 없음
  • 배포 구조 단순

단점:

  • 프론트엔드와 백엔드를 독립적으로 스케일링 불가능
  • 하지만 sQuiry는 소규모 프로젝트라 문제없음

 

향후 개선 방향

프론트엔드 빌드 도구 도입

프로젝트가 커지면 다음을 고려할 수 있다.

  • Vite로 모듈 번들링
  • TypeScript 도입
  • CSS 전처리기 (Sass)

하지만 현재는 복잡도 대비 이점이 크지 않다.

WebSocket 지원

실시간 쿼리 실행 상태나 진행률을 표시하려면 WebSocket이 필요할 수 있다.

사용자 → 쿼리 실행 요청
서버 → WebSocket: "쿼리 파싱 중..."
서버 → WebSocket: "데이터베이스 연결 중..."
서버 → WebSocket: "쿼리 실행 중... (10%)"
서버 → WebSocket: "완료!"

하지만 현재는 쿼리 실행 시간이 짧아 불필요하다.

 

결론

나는 거의 백엔드개발자에 가깝다. 다행히 Claude 의 도움으로 썩 괜찮아 보이는 웹서비스를 만들 수 있었다.
개발하면서 구조에 대해 고민했고, 설계단계에서 많은 시간을 보냈다.

 

핵심 설계 원칙:

  1. 적절한 기술 선택 - 프로젝트 규모에 맞는 도구를 사용했다
  2. 명확한 레이어 분리 - 프론트엔드와 백엔드를 독립적으로 설계했다
  3. 일관된 API 통신 - 모든 API 호출에 동일한 패턴을 적용했다
  4. 사용자 중심 설계 - 단계별 UI와 명확한 피드백을 중점으로 개발했다.

프레임워크는 많은 문제를 해결해주지만, 모든 프로젝트에 필수는 아니라는 생각이든다.

프로젝트의 요구사항을 정확히 파악하고 적절한 수준의 기술을 선택하는 것이 중요할 듯 하다.

반응형