레슨 6 / 8·2개 토픽
커스텀 컴포넌트
AFRAME.registerComponent 심화
A-Frame의 핵심은 커스텀 컴포넌트입니다. AFRAME.registerComponent()로 등록한 컴포넌트는 HTML 속성처럼 어떤 엔티티에든 붙일 수 있습니다. 컴포넌트는 라이프사이클 메서드(init, update, tick, remove, pause, play)를 가지며, schema로 외부에서 속성을 전달받습니다. 잘 설계된 컴포넌트는 재사용 가능하고, 여러 엔티티에 조합하여 복잡한 동작을 만들 수 있습니다.
javascript
// ── 회전 컴포넌트: 매 프레임 엔티티를 회전시킴 ──
AFRAME.registerComponent('spin', {
schema: {
axis: { type: 'string', default: 'y' }, // 회전 축: x, y, z
speed: { type: 'number', default: 1 }, // 초당 회전 속도 (배율)
enabled: { type: 'boolean', default: true }, // 활성화 여부
},
// init: 컴포넌트가 엔티티에 처음 붙을 때 1회 실행
init: function () {
this.rotation = { x: 0, y: 0, z: 0 };
console.log('spin 컴포넌트 초기화:', this.data);
},
// update: schema 속성이 변경될 때 실행 (init 직후에도 호출)
update: function (oldData) {
if (oldData.speed !== this.data.speed) {
console.log('속도 변경:', oldData.speed, '→', this.data.speed);
}
},
// tick: 매 프레임 실행 (~60fps)
// time: 장면 시작 후 경과 시간(ms), delta: 이전 프레임과의 시간 차(ms)
tick: function (time, delta) {
if (!this.data.enabled) return;
const speed = this.data.speed * (delta / 1000) * 360;
const axis = this.data.axis;
const rotation = this.el.getAttribute('rotation');
rotation[axis] = (rotation[axis] + speed) % 360;
this.el.setAttribute('rotation', rotation);
},
// remove: 컴포넌트가 엔티티에서 제거될 때 정리
remove: function () {
console.log('spin 컴포넌트 제거됨');
},
// pause: 장면이나 엔티티가 일시정지될 때
pause: function () {
this.data.enabled = false;
},
// play: 장면이나 엔티티가 재개될 때
play: function () {
this.data.enabled = true;
},
});html
<!-- spin 컴포넌트 사용 -->
<a-box
spin="axis: y; speed: 0.5"
position="0 1 -3"
color="#e74c3c"
></a-box>
<!-- X축 빠른 회전 -->
<a-sphere
spin="axis: x; speed: 2"
position="-3 1.5 -4"
color="#3498db"
radius="0.8"
></a-sphere>
<!-- 회전 비활성화 상태 -->
<a-cylinder
spin="axis: z; speed: 1; enabled: false"
position="3 1 -4"
color="#2ecc71"
></a-cylinder>복합 속성과 실전 컴포넌트
javascript
// ── 따라가기 컴포넌트: 대상 엔티티를 부드럽게 추적 ──
AFRAME.registerComponent('follow', {
schema: {
target: { type: 'selector' }, // 따라갈 대상 (CSS 선택자)
speed: { type: 'number', default: 2 },
offset: { type: 'vec3', default: { x: 0, y: 0, z: -2 } },
},
tick: function (time, delta) {
if (!this.data.target) return;
const targetPos = this.data.target.getAttribute('position');
const currentPos = this.el.getAttribute('position');
const offset = this.data.offset;
const factor = this.data.speed * (delta / 1000);
// 목표 위치 = 대상 위치 + 오프셋
const goalX = targetPos.x + offset.x;
const goalY = targetPos.y + offset.y;
const goalZ = targetPos.z + offset.z;
// 선형 보간(lerp)으로 부드럽게 이동
currentPos.x += (goalX - currentPos.x) * factor;
currentPos.y += (goalY - currentPos.y) * factor;
currentPos.z += (goalZ - currentPos.z) * factor;
this.el.setAttribute('position', currentPos);
},
});
// ── 토글 가시성 컴포넌트 ──
AFRAME.registerComponent('toggle-visible', {
schema: {
event: { type: 'string', default: 'click' },
},
init: function () {
const el = this.el;
el.addEventListener(this.data.event, () => {
const visible = el.getAttribute('visible');
el.setAttribute('visible', !visible);
});
},
});
// ── 거리 기반 크기 변화 컴포넌트 ──
AFRAME.registerComponent('scale-on-distance', {
schema: {
minScale: { type: 'number', default: 0.5 },
maxScale: { type: 'number', default: 2 },
maxDistance: { type: 'number', default: 10 },
},
tick: function () {
const camera = document.querySelector('[camera]');
if (!camera) return;
const camPos = camera.getAttribute('position');
const myPos = this.el.getAttribute('position');
const dx = camPos.x - myPos.x;
const dy = camPos.y - myPos.y;
const dz = camPos.z - myPos.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
const t = Math.min(dist / this.data.maxDistance, 1);
const s = this.data.minScale + t * (this.data.maxScale - this.data.minScale);
this.el.setAttribute('scale', { x: s, y: s, z: s });
},
});html
<!-- follow 컴포넌트: 카메라를 따라다니는 안내 텍스트 -->
<a-entity
text="value: 여기를 봐주세요!; align: center; color: #fff"
follow="target: [camera]; speed: 1; offset: 0 -0.5 -3"
></a-entity>
<!-- scale-on-distance: 가까이 가면 작아지고 멀면 커지는 오브젝트 -->
<a-sphere
scale-on-distance="minScale: 0.3; maxScale: 3; maxDistance: 15"
position="0 2 -8"
color="#9b59b6"
></a-sphere>- •schema — 컴포넌트의 외부 설정값 정의 (type, default 등)
- •type: selector — CSS 선택자로 다른 엔티티를 참조
- •type: vec3 — {x, y, z} 벡터 (HTML에서 "1 2 3"으로 입력)
- •init() — 최초 1회 실행, 이벤트 리스너 등록에 적합
- •tick(time, delta) — 매 프레임 실행, 연속 동작 구현에 사용
- •update(oldData) — 속성 변경 감지, 동적 재설정에 사용
- •remove() — 정리 코드, 이벤트 리스너 해제 등
- •pause() / play() — 엔티티 활성/비활성 시 호출
💡
tick() 안에서 매 프레임 DOM 조회(querySelector)를 하면 성능이 저하됩니다. init()에서 한 번만 조회하고 this에 캐싱하세요. 또한 tick()에서 setAttribute를 과도하게 호출하면 렌더링 성능에 영향을 주므로, 변경이 필요한 경우에만 호출하는 조건 분기를 추가하는 것이 좋습니다.