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 붙이기
- axios 다운로드 (git 또는 npm install axios)
- node_modules/axios 폴더를 프로젝트의 assets/axios/로 복사
- .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대 규칙
- 서비스는 별도 파일·별도 클래스 — 뷰에서 서버 로직 분리
- 표준 응답 스키마 {result, data, error} — 프로젝트 안에서 통일
- HTTP 도구 3종 중 하나 선택 — Va.Http(콜백), Va.HttpRest(REST 명시), Va.HttpSync(async/await)
- axios·fetch도 자유롭게 — 서비스 반환 형태만 통일하면 됨
- 서비스 함수 시그니처는 한 스타일로 통일 — 콜백이든 async든 팀 안에서 하나만
'기본사용법' 카테고리의 다른 글
| VanillaFront 튜토리얼 — 8. 테마 (0) | 2026.09.11 |
|---|---|
| VanillaFront 튜토리얼 — 7. 다국어 (0) | 2026.09.11 |
| VanillaFront 튜토리얼 — 5. MVP 패턴 (0) | 2026.09.11 |
| VanillaFront 튜토리얼 — 4. 라우터 (0) | 2026.09.11 |
| VanillaFront 튜토리얼 — 3. 뷰(View) (0) | 2026.09.11 |