기본사용법

VanillaFront 튜토리얼 — 3. 뷰(View)

VanillaFront 2026. 9. 11. 09:14

https://vanillafront.com 참조

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대 규칙

  1. Va.View를 상속받고 config()에 UI 트리를 반환한다.
  2. 라이프사이클은 6개 — constructor / init / mounted / attached / detached / unmounted.
  3. Va.registerView로 등록하면 어디서든 이름으로 쓸 수 있다.
  4. 뷰는 Shadow DOM으로 격리된다 — CSS·DOM 쿼리 모두 외부와 차단.
  5. getRef는 mounted 이후에 안전하게 사용 가능.