기본사용법

VanillaFront 튜토리얼 — 6. 서비스

VanillaFront 2026. 9. 11. 09:25

https://vanillafront.com 참조

VanillaFront 튜토리얼 — 6. 서비스

MVP의 M(Model) 자리를 담당하는 서비스입니다. 서버 통신·데이터 로직을 뷰에서 분리해서 담는 층이죠. VanillaFront는 서비스 구현을 위한 세 가지 HTTP 도구(Va.Http, Va.HttpRest, Va.HttpSync)와 표준 데이터 프로토콜을 제공하지만, 외부 라이브러리(axios 등)도 자유롭게 섞어 쓸 수 있게 열어놨습니다.


서비스란

이전 MVP 편에서 짚었듯이 서비스는 뷰와 별개로 서버 통신·비즈니스 로직을 담는 독립 모듈입니다.

  • View와 서비스는 N:N 관계 — 한 뷰가 여러 서비스를 쓰고, 한 서비스가 여러 뷰에서 재사용됨
  • Va.Service를 상속받아 만드는 것이 관행 (필수는 아님)
  • 정적(static) 메서드로 노출하는 것이 표준 패턴
// service/CustService.js
export default class CustService extends Va.Service {
    static list(view, params, callback) {
        Va.Http.request(view, '/api/custs', params, {}, callback);
    }
    static save(view, data, callback) {
        Va.Http.request(view, '/api/custs', data, { method: 'POST' }, callback);
    }
}

이렇게 만들어두면 어느 뷰에서든 CustService.list(this, params, cb)로 부를 수 있어요.


JSON 표준 프로토콜

서비스와 서버 사이 데이터 규격을 프로젝트 안에서 통일하면 코드가 매우 깔끔해집니다. VanillaFront가 권장하는 표준 스키마는 이렇습니다.

목록 응답

{
    "result": true,
    "data": {
        "list": [
            { "no": 1, "custName": "홍길동", "age": 20 },
            { "no": 2, "custName": "임꺽정", "age": 42 }
        ]
    },
    "error": { "code": "", "message": "" }
}

단건 응답

{
    "result": true,
    "data": {
        "info": { "no": 1, "custName": "홍길동", "age": 20 }
    },
    "error": { "code": "", "message": "" }
}

목록 + 단건 혼합

{
    "result": true,
    "data": {
        "info": { "no": 1, "custName": "홍길동" },
        "list": [ ... ]
    },
    "error": { "code": "", "message": "" }
}

오류 응답

{
    "result": false,
    "data": { "list": [] },
    "error": {
        "code": "E001",
        "message": "데이터 조회 중 오류가 발생했습니다."
    }
}

왜 이 스키마인가

  • result가 최상위 — 성공/실패를 딱 하나의 boolean으로 판단
  • data  list / info — 페이로드가 무엇이든 예측 가능한 위치
  • error가 별도 — HTTP 상태 코드와 별개로 비즈니스 오류 표현
  • 클라이언트 코드가 res.result  res.data.list  res.error.message 흐름을 반복 → 코드 일관성

서버 개발자와 이 규약을 처음부터 합의하면 개발 속도와 유지보수성이 극적으로 좋아집니다.


세 가지 HTTP 도구

도구언제

Va.Http 기본 비동기 요청. 콜백 방식
Va.HttpRest REST 메서드 명시 (GET/POST/PUT/PATCH/DELETE). 콜백 방식
Va.HttpSync async/await 스타일. 순차 흐름

1) Va.Http — 기본 비동기 콜백

가장 흔히 쓰는 형태. 표준 JSON 프로토콜 응답을 받을 때 편리합니다.

export default class CustService extends Va.Service {
    static list(view, params, callback) {
        const options = {
            method: 'GET',
            headers: { 'Content-Type': 'application/json' }
        };
        Va.Http.request(view, './assets/json/samples/json_data.json',
            params, options, callback);
    }
}

호출 규약: Va.Http.request(view, url, params, options, callback)

  • view — 호출한 뷰 (콜백에서 다시 넘겨받음)
  • url — 서버 엔드포인트
  • params — 요청 파라미터 객체
  • options — method / headers 등
  • callback  (view, ok, resObj, message) 시그니처

뷰에서 사용

class CustList extends Va.View {
    mounted() {
        this.loadData();
    }
    loadData() {
        CustService.list(this, {}, this.onLoaded);
    }
    onLoaded(view, ok, res) {
        if (ok && res.result) {
            view.getRef('grid').setData(res.data.list);
        } else {
            new Va.Alert({
                title: '오류',
                message: res.error?.message || '조회 실패'
            }).show(view);
        }
    }
}

콜백 첫 인자로 view가 다시 넘어와서 this 컨텍스트 문제를 피할 수 있어요.


2) Va.HttpRest — REST 메서드 명시

RESTful API를 다룰 때는 Va.HttpRest가 더 자연스럽습니다. 메서드마다 별도 함수를 제공해요.

GET — 목록/단건 조회

static list(view, addUrl, params, callback) {
    const options = { method: 'GET', headers: { 'Content-Type': 'application/json' } };
    Va.HttpRest.get(view, 'https://api.example.com/posts' + addUrl, params, options,
        (view, ok, resBody, response) => {
            if (ok) {
                // REST 응답을 표준 스키마로 감싸기
                callback(view, true, { data: { list: resBody } }, null);
            } else {
                callback(view, false, resBody,
                    response.status + '(' + response.statusText + ')');
            }
        });
}

콜백 시그니처가 다름: (view, ok, resBodyJson, response) — response 원본까지 넘어옴 (상태 코드 등 접근용).

POST — 신규 등록

static create(view, addUrl, params, callback) {
    const options = { method: 'POST', headers: { 'Content-Type': 'application/json' } };
    Va.HttpRest.post(view, 'https://api.example.com/posts' + addUrl, params, options,
        (view, ok, resBody, response) => { /* ... */ });
}

PUT / PATCH — 수정

  • PUT — 객체의 모든 속성을 덮어씀
  • PATCH — 변경할 속성만 부분 업데이트
static modifyAll(view, addUrl, params, callback) {
    const options = { method: 'PUT', headers: { 'Content-Type': 'application/json' } };
    Va.HttpRest.put(view, url + addUrl, params, options, callback);
}
static modify(view, addUrl, params, callback) {
    const options = { method: 'PATCH', headers: { 'Content-Type': 'application/json' } };
    Va.HttpRest.patch(view, url + addUrl, params, options, callback);
}

DELETE — 삭제

static remove(view, addUrl, params, callback) {
    const options = { method: 'DELETE', headers: { 'Content-Type': 'application/json' } };
    Va.HttpRest.delete(view, url + addUrl, params, options, callback);
}

REST 응답을 표준 스키마로 통일

REST API는 응답 형태가 서비스마다 제각각이므로, 서비스 함수 안에서 표준 스키마로 감싸는 것이 뷰 쪽 코드 일관성에 유리합니다.

if (ok) {
    let responseJson = { result: true, data: { list: resBody }, error: {} };
    callback(view, true, responseJson, null);
}

이렇게 감싸두면 뷰 콜백은 항상 res.data.list로 접근하면 됩니다.


3) Va.HttpSync — async/await 스타일

콜백 지옥이 싫고 순차 흐름을 원한다면 HttpSync. async/await으로 자연스럽게 흐름을 짤 수 있습니다.

서비스

class CustService extends Va.Service {
    static async list(view, params) {
        const options = {
            method: 'GET',
            headers: { 'Content-Type': 'application/json' }
        };
        const response = await Va.HttpSync.request(view,
            './assets/json/samples/json_data.json', params, options);

        // 응답을 그대로 반환할지 프로토콜에 맞게 변환할지는 선택
        return {
            opener: view,
            result: response.result,
            data: response.data,
            message: response.error.message
        };
    }
}

class App extends Va.View {
    onSearch() {
        this.getCustList({});
    }
    async getCustList(params) {
        const response = await CustService.list(this, params);
        if (response.result) {
            this.getRef('custList').setData(response.data.list);
        } else {
            new Va.Alert({ title: '오류', message: response.message }).show(this);
        }
    }
}

언제 쓰나

  • 여러 서비스를 순차 호출해야 할 때 (A 응답이 B 요청에 필요)
  • 에러 처리를 try/catch로 통합하고 싶을 때
  • 비동기 로직이 얽혀 흐름이 안 보일 때
async saveWithValidation() {
    try {
        const valid = await ValidationService.check(this, this.getRef('form').getData());
        if (!valid.result) return;

        const saved = await CustService.save(this, valid.data);
        if (!saved.result) return;

        this.loadList();
    } catch (e) {
        new Va.Alert({ title: '오류', message: e.message }).show(this);
    }
}

외부 라이브러리 — axios

VanillaFront의 HTTP 도구를 안 쓰고 axios·fetch를 그대로 써도 됩니다. 서비스 층만 프로젝트 규약을 따르면 내부 구현은 자유입니다.

axios 붙이기

  1. axios 다운로드 (git 또는 npm install axios)
  2. node_modules/axios 폴더를 프로젝트의 assets/axios/로 복사
  3. .js 파일 그대로 import

설치·번들링·트랜스파일 필요 없습니다. No-Build 원칙 그대로.

서비스

import Va from '../lib/va.js';
import axios from '../assets/axios/dist/esm/axios.js';

export default class FakeService extends Va.Service {
    static async list(view, addUrl, params) {
        try {
            const paramsStr = Va.Util.getParamsStr(params);
            const res = await axios({
                method: 'GET',
                url: 'https://reqres.in/api/users' + addUrl + '?' + paramsStr
            });
            return {
                result: true,
                data: res.data,
                message: res.message
            };
        } catch (error) {
            return {
                result: false,
                data: {},
                message: error.response?.data?.error
                       || (error.message + '(' + error.status + ')')
            };
        }
    }
}

import FakeService from '../../../service/FakeService.js';

class DemoAxios extends Va.View {
    async onSearch() {
        const response = await FakeService.list(this, '', { delay: 3 });
        if (response.result) {
            this.getRef('grid').setData(response.data.data);
        } else {
            new Va.Alert({ title: '오류', message: response.message }).show(this);
        }
    }
}

핵심: axios를 쓰든 fetch를 쓰든 HttpSync를 쓰든 서비스가 반환하는 응답 형태만 프로젝트 표준을 따르면 뷰 코드는 동일합니다. 이것이 서비스 층을 두는 이유이기도 합니다.


서비스 파일 구조 관행

프로젝트 규모에 따라 서비스 폴더를 조정합니다.

소규모 (10~20개 서비스)

service/
├── CustService.js
├── ProductService.js
├── OrderService.js
└── CommonService.js

중대규모 — 도메인별로 그룹

service/
├── cust/
│   ├── CustService.js
│   └── CustHistoryService.js
├── product/
│   ├── ProductService.js
│   └── ProductPriceService.js
├── order/
│   └── OrderService.js
└── common/
    └── CodeService.js

파일명: 대문자 시작 파스칼, ~Service로 끝나는 게 관행.


서비스 함수 시그니처 관행

두 가지 패턴이 굳어져 있습니다.

콜백 방식 (Va.Http / Va.HttpRest)

static list(view, params, callback)
static save(view, data, callback)
static remove(view, id, callback)
  • 첫 인자 view — 콜백 컨텍스트 전달용
  • 마지막 인자 callback(view, ok, res, msg) — 응답 처리 함수

async 방식 (Va.HttpSync / axios)

static async list(view, params)
static async save(view, data)
static async remove(view, id)
  • Promise<response> 반환
  • 호출 쪽에서 await로 받음

한 프로젝트 안에서는 한 가지 스타일로 통일하는 게 좋습니다. 두 방식이 섞여 있으면 팀원마다 다르게 짜서 뷰 코드가 지저분해집니다.


실전 팁

1) 서비스에서 뷰를 직접 갱신하지 말 것

서비스가 응답 처리하면서 view.getRef('grid').setData(...)를 호출하는 건 경계 위반입니다. 서비스는 데이터를 반환/전달만 하고, UI 갱신은 뷰의 콜백 안에서 처리해야 재사용성이 유지됩니다.

2) 공통 에러 처리는 서비스 층에

401 인증 만료, 500 서버 오류 등 프로젝트 전체가 동일하게 처리해야 하는 에러는 서비스 층의 공통 유틸로 뺍니다.

class BaseService extends Va.Service {
    static handleError(view, res, response) {
        if (response.status === 401) {
            Va.setRouterUrl('root', 'login');
            return true;
        }
        return false;
    }
}

3) 로딩 마스킹

긴 요청이 있으면 뷰에 로딩 오버레이를 씌우는 게 UX상 좋습니다.

loadData() {
    this.showMasking();
    CustService.list(this, {}, (view, ok, res) => {
        view.hideMasking();
        // ...
    });
}

Va.View가 제공하는 showMasking() / hideMasking()을 활용.

4) 서비스 재사용 시 URL 파라미터화

같은 REST 엔드포인트의 다른 하위 경로(예: /posts/1, /posts/2)를 위해 서비스 함수에 addUrl 인자를 두는 패턴이 편합니다.

static get(view, addUrl, params, callback) {
    Va.HttpRest.get(view, baseUrl + addUrl, params, options, callback);
}
// 호출
PostService.get(this, '/' + postId, {}, this.onLoaded);

5) 프로토콜 스키마 헬퍼

응답 구조 접근이 반복되면 아예 헬퍼를 만들어 두는 것도 방법:

class Response {
    static list(res) { return res?.data?.list || []; }
    static info(res) { return res?.data?.info || null; }
    static error(res) { return res?.error?.message || ''; }
}

요약 — 서비스 5대 규칙

  1. 서비스는 별도 파일·별도 클래스 — 뷰에서 서버 로직 분리
  2. 표준 응답 스키마 {result, data, error} — 프로젝트 안에서 통일
  3. HTTP 도구 3종 중 하나 선택  Va.Http(콜백), Va.HttpRest(REST 명시), Va.HttpSync(async/await)
  4. axios·fetch도 자유롭게 — 서비스 반환 형태만 통일하면 됨
  5. 서비스 함수 시그니처는 한 스타일로 통일 — 콜백이든 async든 팀 안에서 하나만