https://vanillafront.com 참조
VanillaFront 튜토리얼 — 2. 컴포넌트
첫 편에서 "화면은 JSON 트리로 선언한다"고 이야기했습니다. 이번엔 그 트리의 벽돌 하나 — 컴포넌트가 어떻게 정의되고 어떻게 조립되는지 파고듭니다.
컴포넌트란
VanillaFront에서 화면을 구성하는 모든 UI 단위입니다. 단순한 <div> 하나부터 그리드·다이어그램 같은 복잡한 위젯까지 모두 "컴포넌트"라는 같은 개념으로 다룹니다.
특징을 나열하면:
- 하나의 클래스로 정의된 객체입니다. React·Vue가 태그 중심이라면 VanillaFront는 객체 중심입니다.
- 1개 또는 여러 개의 element로 구성됩니다. 그리드처럼 내부에 수십 개 DOM 요소를 가진 것도 하나의 컴포넌트로 취급합니다.
- 누구나 새로 만들거나 상속받아 확장할 수 있습니다.
- JSON 태그 형태로 선언하거나 new로 동적 생성할 수 있습니다.
- 자체 elements 외에 다른 컴포넌트를 안에 품는 innerComponents 개념을 갖습니다.
- elements, properties, events, innerComponents를 공개함으로써 외부에서 자유롭게 제어할 수 있습니다. (그리드처럼 복잡한 컴포넌트는 일부만 공개하기도 합니다.)
- 트리 구조로 부모-자식 관계를 갖습니다.
- 자식은 여러 개, 부모는 하나만 가질 수 있습니다.
HTML 태그가 아니라 JSON을 쓰는 이유
일반 HTML로 빨간 div를 쓰면:
<div style="color:red">
div영역
</div>
VanillaFront에서는:
{
tagName: 'div',
innerHTML: 'DIV 영역',
style: 'color:red'
}
이 방식으로 표현하는 이유는 두 가지입니다.
- JavaScript는 XML(HTML) 태그보다 JSON을 다루는 게 훨씬 편합니다. 값 대입, 조건 분기, 변수 참조가 자연스럽게 됩니다.
- JSX 같은 비표준 문법을 도입하지 않아도 됩니다. 표준 JSON 객체이므로 컴파일러·트랜스파일러가 필요 없습니다.
HTML의 태그명 div는 그대로 tagName: 'div'가 되고, 태그 속성은 객체 속성이 됩니다. 매우 직관적입니다.
부모-자식 관계
컴포넌트는 다른 컴포넌트를 포함하거나 포함될 수 있습니다.
{
tagName: 'div', // 부모
tags: [
{ tagName: 'h1', innerHTML: '제목' }, // 자식 1
{ tagName: 'p', innerHTML: '본문' }, // 자식 2
{
tagName: 'div', // 자식 3 (내부에 또 자식)
tags: [
{ tagName: 'button', text: '확인' }
]
}
]
}
- 부모 컴포넌트는 자식 목록을 tags로 선언합니다.
- 자식 컴포넌트는 부모 하나만 가질 수 있습니다.
- 결과적으로 화면은 하나의 최상위 컴포넌트에서 시작해 트리 형태로 펼쳐집니다.
컴포넌트의 6가지 구성 요소
컴포넌트 클래스는 자기 자신을 이렇게 소개합니다.
구성 요소뜻
| elements | 자기가 직접 만든 DOM 요소 목록 (예: element, inner, text, icon) |
| properties | 지원하는 옵션 속성 목록 (예: text, icon, size, disabled) |
| events | 발생시키는 커스텀 이벤트 목록 (예: click, focus, blur) |
| innerComponents | 안에 품고 있는 다른 컴포넌트 목록 (예: DatePicker의 menuButton) |
| methods | 외부에서 호출 가능한 메서드 (예: setText, focus) |
| CSS 파일 | 컴포넌트별로 이미 스타일이 정의되어 있음 |
Va.Button으로 예를 들어 보면 대략 이렇습니다:
Va.Button = class extends Va.Component {
constructor(option) {
super('button', option);
this.tagName = 'button';
this.elements = [...this.elements, 'element', 'inner', 'icon', 'text'];
this.properties = [...this.properties, 'text', 'icon', 'iconPosition', 'size', 'disabled'];
this.events = [...this.events, 'click', 'focus', 'blur', 'contextmenu'];
this.setOption();
// DOM 생성, 이벤트 바인딩...
}
update() { /* 상태 → DOM 반영 */ }
};
Va.registerComponent('button', Va.Button);
- Va.Component를 상속받고
- 이 클래스의 elements / properties / events를 배열에 등록하고
- Va.registerComponent('button', ...) 로 등록하면 이제 다른 곳에서 tagName: 'button'으로 사용할 수 있습니다.
이벤트 — on 접두어 하나로 받는다
VanillaFront의 이벤트는 DOM 이벤트가 아니라 컴포넌트 이벤트입니다. 이유는 간단해요 — 하나의 컴포넌트가 여러 element로 이루어져 있을 수 있어서, 그 컴포넌트를 대표하는 사용자 이벤트가 필요하기 때문입니다.
규칙
- 컴포넌트는 자신이 발생시키는 이벤트를 events에 등록합니다.
- View에서 on + 이벤트명(대문자 시작) 형태의 문자열로 콜백 메서드를 연결합니다.
class App extends Va.View {
onButtonClick(btn, element, evt) {
new Va.Alert({
title: '확인',
message: '버튼을 클릭했습니다.'
}).show(this);
}
config() {
return {
tagName: 'div',
tags: [{
tagName: 'button',
text: '버튼',
ref: 'button',
onClick: 'onButtonClick' // ← 문자열로 메서드명 연결
}]
};
}
}
onClick: 'onButtonClick' — 이렇게 문자열로 연결하는 게 특징입니다. onClick은 click 이벤트의 콜백 자리입니다.
코드에서 직접 받고 싶다면
mounted() {
this.getRef('button').addEventListener('click', (btn, element, evt) => {
new Va.Alert({ title: '확인', message: '버튼을 클릭했습니다.' }).show(this);
});
}
이 addEventListener는 DOM의 것이 아니라 컴포넌트의 것입니다. 따라서 파라미터도 컴포넌트가 정의한 btn, element, evt 순서 그대로 들어옵니다.
지원 안 하는 이벤트를 받고 싶다면
컴포넌트가 노출하지 않은 DOM 표준 이벤트(예: mouseenter)를 받으려면:
this.getRef('button').element.addEventListener('mouseenter', (evt) => {
// ...
});
.element를 거쳐야 표준 DOM 이벤트 시스템으로 접근할 수 있습니다.
innerComponents — 자식과는 다른 개념
자식 컴포넌트(tags:[...])와 헷갈리기 쉬운 개념입니다.
- 자식 컴포넌트 — 사용자가 배치한 별개의 컴포넌트
- 내부 컴포넌트(innerComponent) — 컴포넌트 자기 자신의 일부로 미리 만들어져 있는 하위 컴포넌트
예 — DatePicker
Va.DatePicker는 안에 캘린더 팝업을 여는 버튼을 갖고 있는데, 이 버튼은 이미 존재하는 Va.MenuButton을 재활용합니다. 매번 element로 다시 짤 필요 없이 컴포넌트를 조립하는 셈이죠.
Va.DatePicker = class extends Va.PureField {
constructor(option) {
super('input', option);
this.innerComponents = [...this.innerComponents,
'menuButton', 'buttonLeft', 'buttonRight', 'buttonToday'];
// ...
this.menuButtonComponent = new Va.MenuButton({ innerComponent: true, ... });
}
};
접근 규칙
내부 컴포넌트에는 이름 + Component 형태로 접근합니다.
this.getRef('myDate').menuButtonComponent // ← DatePicker 안 MenuButton 인스턴스
특징
- tags가 아니라 옵션 객체 형태로 재정의합니다.
- 자식 컴포넌트가 아니므로 부모-자식 관계에 잡히지 않습니다.
- 자신의 이벤트를 기본적으로는 View에 전달하지 않습니다. 필요하면 부모 컴포넌트가 재발화해 줍니다.
- 옵션으로 재정의해서 세부 스타일/동작을 커스터마이즈할 수 있습니다.
{
tagName: 'datePicker',
menuButton: { // ← 내부 컴포넌트 옵션 재정의
appearance: 'primary',
popWidth: 320
}
}
ref로 컴포넌트 참조하기
컴포넌트에 ref: '이름'을 붙이면 View 내부에서 getRef('이름')으로 그 컴포넌트 인스턴스를 가져올 수 있습니다.
config() {
return {
tagName: 'div',
tags: [
{ tagName: 'input', ref: 'name' },
{ tagName: 'button', text: '전송', onClick: 'onSubmit' }
]
};
},
onSubmit(btn, el, evt) {
const name = this.getRef('name').getValue();
console.log(name);
}
ref는 컴포넌트에 이름표를 붙이는 것과 같습니다. View의 콜백들이 이 이름으로 컴포넌트를 찾아 조작합니다.
커스텀 컴포넌트 만들기
기본 제공 컴포넌트로 부족하면 직접 정의하면 됩니다.
Va.MyBadgeButton = class extends Va.Component {
constructor(option) {
super('button', option);
this.tagName = 'myBadgeButton';
this.elements = [...this.elements, 'element', 'label', 'badge'];
this.properties = [...this.properties, 'label', 'count'];
this.events = [...this.events, 'click'];
this.setOption();
this.labelElement = document.createElement('span');
this.labelElement.setAttribute('elname', 'label');
this.badgeElement = document.createElement('span');
this.badgeElement.setAttribute('elname', 'badge');
this.element.append(this.labelElement, this.badgeElement);
this._bindEvent(this.element, 'click', (evt) => {
this.dispatchEvent('click', this, this.element, evt);
});
}
update() {
this.labelElement.textContent = this.label ?? '';
this.badgeElement.textContent = this.count ?? '';
super.update();
}
};
Va.registerComponent('myBadgeButton', Va.MyBadgeButton);
이제 어디에서든:
{
tagName: 'myBadgeButton',
label: '알림',
count: 5,
onClick: 'onOpen'
}
등록 한 번이면 나머지 프로젝트 전체에서 표준 컴포넌트처럼 씁니다.
컴포넌트에 CSS 적용하기
컴포넌트는 각자 CSS가 이미 붙어 있어 대부분 추가 스타일링이 필요 없지만, 커스텀이 필요할 때는 두 가지 방법이 있습니다.
1) style 속성 — 인라인 스타일
{ tagName: 'div', style: 'color:red;padding:10px' }
// 또는
{ tagName: 'div', style: { color: 'red', padding: '10px' } }
작은 조정에 적합합니다.
2) class / addClass 속성
{ tagName: 'div', innerHTML: '내용', class: 'myclass', addClass: 'myaddclass' }
- class — 기존 클래스를 대체
- addClass — element.classList.add()와 동일하게 추가
3) 내부 element에 개별 클래스 부여
mounted() {
this.getRef('button').textElement.classList.add('myclass');
this.getRef('button').iconElement.classList.add('myclass2');
}
추천은 여전히 CSS 파일 직접 수정입니다. 퍼블리셔가 프로젝트 CSS를 관리하는 게 유지보수에 유리합니다.
요약 — 컴포넌트 4대 규칙
- 컴포넌트는 클래스로 정의된 객체다. 태그가 아니다.
- 화면에는 JSON 트리로 선언된다. { tagName, tags, on..., style, ref, ... }
- 이벤트는 on 접두어로 받는다. View의 메서드 이름을 문자열로 지정.
- ref로 어디서든 참조할 수 있다.
이 네 가지만 손에 익으면 VanillaFront로 화면 짜는 감이 잡힙니다.
다음 편 예고
다음 편은 뷰(View) — 컴포넌트가 벽돌이면 뷰는 방(room). 화면 단위인 Va.View가 어떻게 라이프사이클을 관리하고, 자식 뷰를 어떻게 연결하며, mounted가 뭐고 getRef가 언제 유효한지까지 다루겠습니다.
질문·수정 사항 있으면 댓글 남겨주세요. 다음 편에서 다시 봅니다.
이 정도 톤과 분량으로 이어갈까요? 다음 편(뷰) 준비하기 전에 이 편에 손볼 부분 있으면 알려주세요.
'기본사용법' 카테고리의 다른 글
| VanillaFront 튜토리얼 — 6. 서비스 (0) | 2026.09.11 |
|---|---|
| VanillaFront 튜토리얼 — 5. MVP 패턴 (0) | 2026.09.11 |
| VanillaFront 튜토리얼 — 4. 라우터 (0) | 2026.09.11 |
| VanillaFront 튜토리얼 — 3. 뷰(View) (0) | 2026.09.11 |
| VanillaFront 튜토리얼 — 1. 시작하기 (0) | 2026.09.11 |