VanillaFront 튜토리얼 — 3. 뷰(View)
VanillaFront 튜토리얼 — 3. 뷰(View)
컴포넌트가 벽돌이라면, **뷰는 방(room)**입니다. 화면 하나를 담는 단위 — 그 안에 컴포넌트를 배치하고, 라이프사이클을 갖고, 다른 뷰와 연결됩니다. 이번 편에서는 Va.View가 어떻게 만들어지고 관리되는지 정리합니다.
뷰란
화면을 이루는 기본 단위입니다. 전체 페이지 하나가 뷰가 될 수도 있고, 페이지 안의 작은 영역이 뷰가 될 수도 있어요. 한 뷰가 다른 뷰를 자식으로 갖는 것도 가능합니다.
- 하나의 뷰는 하나의 최상위 컴포넌트로 시작합니다.
- 그 아래로 자식 컴포넌트를 배치해 화면을 구성합니다.
- 화면 조작·데이터·이벤트 처리는 뷰 단위로 개발합니다.
- SPA(Single Page Application) 답게 동적으로 추가·교체·제거 가능합니다.
Va.View 상속
모든 뷰는 Va.View를 상속받아 만듭니다.
export default class MyPage extends Va.View {
constructor() {
super(arguments);
}
config() {
return {
tagName: 'div',
style: 'width:600px',
layout: 'ds-flex fd-column gap-s',
tags: [
{ tagName: 'inputField', label: '이름' },
{ tagName: 'button', text: '저장', ref: 'btn' }
]
};
}
}
필수 요소는 딱 두 가지:
- constructor() — super(arguments) 호출
- config() — UI 트리를 JSON으로 반환
이게 전부입니다. 라이프사이클 훅이나 이벤트 핸들러는 필요할 때만 추가하면 됩니다.
뷰의 라이프사이클
VanillaFront는 최소한의 라이프사이클을 유지합니다. 6개의 훅만 기억하면 됩니다.
생성 단계
constructor → init → (config 렌더링) → mounted → attached
훅언제 호출되나
| constructor() | JS 표준 생성자. 인스턴스 만들어질 때 |
| init() | config() 실행되기 전 — 초기 데이터·서비스 세팅 자리 |
| mounted() | config() 렌더링 후 — DOM이 만들어진 상태. getRef() 호출 가능 시점 |
| attached() | 뷰가 실제 영역에 append되었을 때 |
소멸 단계
unmount() → detached → unmounted
훅언제 호출되나
| unmount() | 뷰를 명시적으로 제거하는 트리거 (this.unmount()로 호출) |
| detached() | 영역에서 분리될 때 |
| unmounted() | 최종 정리 시점. 타이머·리스너 등 리소스 정리 자리 |
사이클을 눈으로 확인
class App extends Va.View {
constructor() { super(arguments); console.log('constructor'); }
init() { console.log('init'); }
mounted() { console.log('mounted'); }
attached() { console.log('attached'); }
detached() { console.log('detached'); }
unmounted() { console.log('unmounted'); }
onUnmount() { this.unmount(); }
config() {
return {
tagName: 'div',
tags: [{
tagName: 'button',
text: 'unmount',
onClick: 'onUnmount'
}]
};
}
}
new App();
중요 포인트:
- init은 DOM 렌더링 전 — 여기서 getRef() 호출하면 아직 없습니다.
- mounted가 되어야 DOM이 완성된 상태 — getRef(), 자식 컴포넌트 조작이 안전한 시점.
- 이벤트가 아니라 메서드 호출입니다. 오버라이드하면 자동으로 불려요.
뷰 등록 — Va.registerView
뷰를 매번 import → new로 만드는 건 번거롭습니다. Va.registerView로 한 번 등록하면 프로젝트 어디서든 이름으로 부를 수 있습니다.
Va.registerView('/view/order/OrderList', OrderList);
이후에는 두 가지 방식으로 사용 가능:
1) tagName 문자열로 부모 뷰의 config에 삽입
config() {
return {
tagName: 'div',
tags: [
{ tagName: '/view/order/OrderList' } // ← 등록된 뷰가 자식으로 붙음
]
};
}
2) 동적으로 생성해서 영역에 추가
const orderView = new (Va.getView('/view/order/OrderList'))();
Va.setRouterAreaAsName(orderView, 'main');
라우터와 결합하면 URL 이동에 맞춰 뷰가 자동 교체됩니다.
Shadow DOM으로 독립성 확보
뷰는 자기 안의 DOM과 CSS가 부모/외부에 새어 나가지 않게 격리됩니다. 이걸 위해 뷰의 최상위 element는 #shadow-root(open)을 가집니다.
<div id="app"> ← 호스트 엘리먼트
#shadow-root (open) ← Shadow DOM 경계
<style>va.css</style> ← Adopted StyleSheets
<style>va-light.css</style>
<article class="va-article"> ← 뷰의 루트
<section>...</section>
</article>
</div>
이 격리가 주는 이점
- CSS 충돌 없음 — 뷰마다 다른 스타일을 써도 서로 간섭 안 함
- DOM 쿼리 격리 — document.querySelector로 뷰 내부가 조회되지 않음 → 실수 방지
- 쿼리 속도 향상 — 검색 범위가 뷰 안으로 국한됨
- 컴포넌트 재사용 안정 — 어디에 넣어도 원래 스타일 그대로 유지
언제 유의해야 하나
부모 페이지의 CSS 셀렉터가 뷰 안 요소에 걸리지 않는다는 걸 잊고 코딩하면 스타일이 왜 안 먹는지 헤맬 수 있어요. 뷰 내부 스타일은 뷰 안에서 처리해야 합니다.
ref로 컴포넌트 접근
뷰 안 컴포넌트에 ref 이름을 붙이면 뷰 메서드 어디서든 접근할 수 있습니다.
class App extends Va.View {
onSave(btn, el, evt) {
const name = this.getRef('nameInput').getValue();
alert(name);
}
config() {
return {
tagName: 'div',
tags: [
{ tagName: 'inputField', label: '이름', ref: 'nameInput' },
{ tagName: 'button', text: '저장', onClick: 'onSave' }
]
};
}
}
관련 메서드
메서드설명
| getRef(name) | 이름으로 컴포넌트 하나 조회 |
| getRefs(name) | 같은 이름 여러 개 조회 (동적 추가 상황) |
| getParentView() | 부모 뷰 반환 (자식 뷰에서 사용) |
| getParams() / setParams(params, opener) | 뷰 간 파라미터 전달 |
| showMasking() / hideMasking() | 로딩 오버레이 |
| unmount() | 뷰 제거 트리거 |
자식 뷰 — 뷰 안에 뷰
뷰는 다른 뷰를 자식으로 품을 수 있습니다.
class MainLayout extends Va.View {
config() {
return {
tagName: 'page',
tags: [
{ tagName: 'panel', tags: [{ tagName: 'h1', innerHTML: '주문 관리' }] },
{
tagName: 'panel',
tags: [
{ tagName: '/view/order/OrderList' } // ← 자식 뷰
]
}
]
};
}
}
자식 뷰에서 부모 뷰의 메서드를 호출하고 싶다면:
class OrderList extends Va.View {
onRefresh() {
const parent = this.getParentView();
parent.doSomething();
}
}
init vs mounted — 자주 헷갈리는 지점
두 훅은 서로 다른 목적을 갖습니다.
init() — 렌더링 전
- 서비스 인스턴스 생성
- 초기 데이터 로드 요청
- 뷰 내부 상태값 초기화
init() {
this.orderService = new OrderService();
this.selectedId = null;
}
주의: 이 시점에 getRef()는 아직 컴포넌트가 없어서 동작 안 합니다.
mounted() — 렌더링 후
- 초기 포커스 지정
- 서버 데이터로 그리드 채우기
- DOM 크기 계산이 필요한 로직
mounted() {
this.getRef('nameInput').focus();
this.loadOrderList();
}
이 시점부터 getRef()가 유효합니다.
Adopted StyleSheets — 뷰 전용 CSS
뷰 자체의 CSS를 붙이려면 addStyleSheet 또는 addStyleLink를 씁니다.
class MyPage extends Va.View {
constructor() {
super(arguments);
this.addStyleLink('./view/mypage/mypage.css');
}
config() { ... }
}
Shadow DOM 안으로만 스타일이 적용되므로 다른 뷰에 새어 나가지 않아요.
라우터의 기본 단위
뷰는 라우터의 이동 대상이 됩니다. URL의 hash가 바뀌면 등록된 라우터 경로에 매핑된 뷰가 지정된 영역에 표시됩니다.
// router.js
Va.setRouterPath([
{ path: '/order/list', view: '/view/order/OrderList', area: 'main' },
{ path: '/order/detail', view: '/view/order/OrderDetail', area: 'main' }
]);
http://example.com/#/order/list 로 접속하면 OrderList 뷰가 main 영역에 뜹니다.
라우터 자체는 다음 편에서 자세히 다룹니다.
뷰 만들 때 자주 쓰는 패턴
표준 뷰 스켈레톤
import Va from '../../lib/va.js';
export default class OrderList extends Va.View {
constructor() {
super(arguments);
}
init() {
// 서비스·상태 초기화
}
mounted() {
// 초기 데이터 로드
this.loadData();
}
onSearch(btn, el, evt) {
const keyword = this.getRef('searchInput').getValue();
this.loadData(keyword);
}
loadData(keyword) {
// 서비스 호출...
}
config() {
return {
tagName: 'page',
tags: [{
tagName: 'panel',
tags: [
{ tagName: 'searchField', ref: 'searchInput', label: '검색' },
{ tagName: 'button', text: '조회', onClick: 'onSearch' },
{ tagName: 'grid', ref: 'refGrid', columns: [ ... ] }
]
}]
};
}
}
Va.registerView('/view/order/OrderList', OrderList);
이 패턴 하나가 실무의 90%를 커버합니다.
Page > Panel 컨벤션
VanillaFront 화면의 표준 컨테이너 구조는 page + panel 입니다.
- page — 최상위 화면 컨테이너
- panel — 섹션 분할 컨테이너
둘 다 자체적으로 gap을 가지므로, 두 태그만 잘 조합하면 여백이 자연스럽게 잡힙니다. 별도 margin·padding을 강제로 넣지 마세요 — 룩이 오히려 어긋납니다.
요약 — 뷰 5대 규칙
- Va.View를 상속받고 config()에 UI 트리를 반환한다.
- 라이프사이클은 6개 — constructor / init / mounted / attached / detached / unmounted.
- Va.registerView로 등록하면 어디서든 이름으로 쓸 수 있다.
- 뷰는 Shadow DOM으로 격리된다 — CSS·DOM 쿼리 모두 외부와 차단.
- getRef는 mounted 이후에 안전하게 사용 가능.