컴포넌트
구조
컴포넌트는 코드의 구성 및 재사용성을 높이기 위해 뷰의 일부분을 캡슐화하는 방법입니다.
view
메서드를 가진 JavaScript 객체는 Mithril.js 컴포넌트입니다. 컴포넌트는 m()
유틸리티를 통해 사용할 수 있습니다.
// 컴포넌트 정의
var Example = {
view: function (vnode) {
return m('div', 'Hello');
},
};
// 컴포넌트 사용
m(Example);
// HTML 결과:
// <div>Hello</div>
라이프사이클 메서드
컴포넌트는 가상 DOM 노드와 동일한 라이프사이클 메서드를 가질 수 있습니다. vnode
는 각 라이프사이클 메서드와 view
에 인수로 전달되며, onbeforeupdate
에는 이전 vnode가 추가로 전달됩니다.
var ComponentWithHooks = {
oninit: function (vnode) {
console.log('initialized');
},
oncreate: function (vnode) {
console.log('DOM created');
},
onbeforeupdate: function (newVnode, oldVnode) {
return true;
},
onupdate: function (vnode) {
console.log('DOM updated');
},
onbeforeremove: function (vnode) {
console.log('exit animation can start');
return new Promise(function (resolve) {
// 애니메이션 완료 후 호출
resolve();
});
},
onremove: function (vnode) {
console.log('removing DOM element'); // DOM 요소 제거 중
},
view: function (vnode) {
return 'hello';
},
};
다른 유형의 가상 DOM 노드와 마찬가지로 컴포넌트는 vnode 타입으로 사용될 때 추가 라이프사이클 메서드를 정의할 수 있습니다.
function initialize(vnode) {
console.log('initialized as vnode'); // vnode로 초기화됨
}
m(ComponentWithHooks, { oninit: initialize });
vnode의 라이프사이클 메서드는 컴포넌트 메서드를 오버라이드하지 않으며, 컴포넌트 메서드 또한 vnode의 라이프사이클 메서드를 오버라이드하지 않습니다. 컴포넌트 라이프사이클 메서드는 항상 vnode의 해당 메서드 다음에 실행됩니다.
vnode에서 사용자 정의 콜백 함수 이름에 라이프사이클 메서드 이름을 사용하지 않도록 주의하십시오.
라이프사이클 메서드에 대한 자세한 내용은 라이프사이클 메서드 페이지를 참조하십시오.
컴포넌트에 데이터 전달
데이터는 하이퍼스크립트 함수에서 두 번째 매개변수로 attrs
객체를 전달하여 컴포넌트 인스턴스에 전달할 수 있습니다.
m(Example, { name: 'Floyd' });
이 데이터는 vnode.attrs
를 통해 컴포넌트의 뷰 또는 라이프사이클 메서드에서 접근할 수 있습니다.
var Example = {
view: function (vnode) {
return m('div', 'Hello, ' + vnode.attrs.name);
},
};
참고: 라이프사이클 메서드는 attrs
객체에서도 정의할 수 있으므로 Mithril.js 자체에서 호출됩니다. 따라서 사용자 정의 콜백에 해당 이름을 사용하지 않아야 합니다. 라이프사이클 메서드로 사용하려는 경우에만 attrs
에서 사용하십시오.
상태
모든 가상 DOM 노드와 마찬가지로 컴포넌트 vnode는 상태를 가질 수 있습니다. 컴포넌트 상태는 객체 지향 아키텍처, 캡슐화 및 관심사 분리를 지원하는 데 유용합니다.
다른 많은 프레임워크와 달리 컴포넌트 상태를 변경해도 다시 그리기 또는 DOM 업데이트가 자동으로 트리거되지 않습니다. 다시 그리기는 이벤트 핸들러가 실행될 때, m.request에 의해 만들어진 HTTP 요청이 완료될 때 또는 브라우저가 다른 경로로 이동할 때 수행됩니다. Mithril.js의 컴포넌트 상태 메커니즘은 애플리케이션의 편의를 위해 제공됩니다.
위의 조건에 해당하지 않는 상태 변경이 발생하면 (예: setTimeout
후) m.redraw()
를 사용하여 수동으로 다시 그리기를 트리거할 수 있습니다.
클로저 컴포넌트 상태
위의 예에서 각 컴포넌트는 POJO (Plain Old JavaScript Object)로 정의되며, 이는 Mithril.js에서 해당 컴포넌트 인스턴스의 프로토타입으로 내부적으로 사용됩니다. POJO로 컴포넌트 상태를 사용할 수 있지만 (아래에서 설명), 가장 깔끔하거나 간단한 접근 방식은 아닐 수 있습니다. 더 나은 방법은 POJO 컴포넌트 인스턴스를 반환하는 래퍼 함수를 사용하여 자체적인 클로저 스코프를 갖도록 하는 것입니다.
클로저 컴포넌트를 사용하면 외부 함수 내에서 선언된 변수를 통해 상태를 간단히 유지할 수 있습니다.
function ComponentWithState(initialVnode) {
// 컴포넌트 상태 변수, 각 인스턴스에 고유함
var count = 0;
// POJO 컴포넌트 인스턴스: vnode를 반환하는
// view 함수가 있는 모든 객체
return {
oninit: function (vnode) {
console.log('init a closure component'); // 클로저 컴포넌트 초기화
},
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + count),
m(
'button',
{
onclick: function () {
count += 1;
},
},
'Increment count' // 카운트 증가
)
);
},
};
}
클로저 내에서 선언된 모든 함수는 해당 상태 변수에 접근할 수 있습니다.
function ComponentWithState(initialVnode) {
var count = 0;
function increment() {
count += 1;
}
function decrement() {
count -= 1;
}
return {
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + count),
m(
'button',
{
onclick: increment,
},
'Increment'
),
m(
'button',
{
onclick: decrement,
},
'Decrement'
)
);
},
};
}
클로저 컴포넌트는 POJO와 동일한 방식으로 사용됩니다 (예: m(ComponentWithState, { passedData: ... })
).
클로저 컴포넌트의 큰 장점은 이벤트 핸들러 콜백을 연결할 때 this
바인딩에 대해 걱정할 필요가 없다는 것입니다. 실제로 this
는 전혀 사용되지 않으며 this
컨텍스트의 모호성에 대해 생각할 필요가 없습니다.
POJO 컴포넌트 상태
일반적으로 컴포넌트 상태를 관리할 때 클로저를 사용하는 것이 좋습니다. 하지만 POJO에서 상태를 관리해야 하는 경우, 컴포넌트의 상태는 초기화 시의 청사진, vnode.state
, 그리고 컴포넌트 메서드 내에서의 this
키워드를 통해 접근할 수 있습니다.
초기화 시
POJO 컴포넌트의 경우 컴포넌트 객체는 각 컴포넌트 인스턴스의 프로토타입이므로 컴포넌트 객체에 정의된 모든 속성은 vnode.state
의 속성으로 접근할 수 있습니다. 이를 통해 간단한 "청사진" 상태 초기화가 가능합니다.
아래 예에서 data
는 ComponentWithInitialState
컴포넌트의 vnode.state
객체의 속성이 됩니다.
var ComponentWithInitialState = {
data: 'Initial content',
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithInitialState);
// HTML 결과:
// <div>Initial content</div>
vnode.state를 통해
보시다시피 상태는 컴포넌트의 모든 라이프사이클 메서드와 view
메서드에서 사용할 수 있는 vnode.state
속성을 통해 접근할 수도 있습니다.
var ComponentWithDynamicState = {
oninit: function (vnode) {
vnode.state.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithDynamicState, { text: 'Hello' });
// HTML 결과:
// <div>Hello</div>
this 키워드를 통해
상태는 컴포넌트의 모든 라이프사이클 메서드와 view
메서드에서 사용할 수 있는 this
키워드를 통해 접근할 수도 있습니다.
var ComponentUsingThis = {
oninit: function (vnode) {
this.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', this.data);
},
};
m(ComponentUsingThis, { text: 'Hello' });
// HTML 결과:
// <div>Hello</div>
ES5 함수를 사용할 때 중첩된 익명 함수에서 this
의 값은 컴포넌트 인스턴스가 아닙니다. 이 JavaScript 한계를 해결하는 두 가지 권장 방법은 화살표 함수를 사용하거나, 지원되지 않는 경우 vnode.state
를 사용하는 것입니다.
클래스
객체 지향 프로젝트와 같이 필요에 따라 클래스를 사용하여 컴포넌트를 작성할 수도 있습니다.
class ClassComponent {
constructor(vnode) {
this.kind = 'class component';
}
view() {
return m('div', `Hello from a ${this.kind}`);
}
oncreate() {
console.log(`A ${this.kind} was created`);
}
}
클래스 컴포넌트는 트리를 렌더링하기 위해 .prototype.view
를 통해 감지되는 view()
메서드를 정의해야 합니다.
일반 컴포넌트와 동일한 방식으로 사용할 수 있습니다.
// 예: m.render를 통해
m.render(document.body, m(ClassComponent));
// 예: m.mount를 통해
m.mount(document.body, ClassComponent);
// 예: m.route를 통해
m.route(document.body, '/', {
'/': ClassComponent,
});
// 예: 컴포넌트 구성
class AnotherClassComponent {
view() {
return m('main', [m(ClassComponent)]);
}
}
클래스 컴포넌트 상태
클래스를 사용하면 클래스 인스턴스 속성 및 메서드로 상태를 관리하고 this
를 통해 접근할 수 있습니다.
class ComponentWithState {
constructor(vnode) {
this.count = 0;
}
increment() {
this.count += 1;
}
decrement() {
this.count -= 1;
}
view() {
return m(
'div',
m('p', 'Count: ', this.count),
m(
'button',
{
onclick: () => {
this.increment();
},
},
'Increment'
),
m(
'button',
{
onclick: () => {
this.decrement();
},
},
'Decrement'
)
);
}
}
this
컨텍스트를 올바르게 참조하려면 이벤트 핸들러 콜백에 화살표 함수를 사용해야 합니다.
컴포넌트 종류 혼합
컴포넌트는 자유롭게 혼합할 수 있습니다. 클래스 컴포넌트는 클로저 또는 POJO 컴포넌트를 자식으로 가질 수 있습니다.
특수 속성
Mithril.js는 특정 속성 키에 특별한 의미를 부여하므로, 일반적인 컴포넌트 속성으로 사용하는 것을 지양해야 합니다.
- 라이프사이클 메서드:
oninit
,oncreate
,onbeforeupdate
,onupdate
,onbeforeremove
및onremove
key
: 키가 있는 조각에서 ID를 추적하는 데 사용됩니다.tag
: vnode를 일반 속성 객체 및 vnode가 아닌 다른 항목과 구별하는 데 사용됩니다.
안티 패턴 방지
Mithril.js는 유연하지만, 몇몇 코드 패턴은 권장되지 않습니다.
뚱뚱한 컴포넌트 방지
일반적으로 "뚱뚱한" 컴포넌트는 사용자 정의 인스턴스 메서드가 있는 컴포넌트입니다. 즉, 함수를 vnode.state
또는 this
에 연결하지 않아야 합니다. 논리적으로 컴포넌트 인스턴스 메서드에 적합하고 다른 컴포넌트에서 재사용할 수 없는 논리가 있는 경우는 매우 드뭅니다. 해당 논리가 나중에 다른 컴포넌트에서 필요할 수 있는 경우는 비교적 일반적입니다.
해당 논리가 컴포넌트 상태에 연결된 경우보다 데이터 레이어에 배치되면 코드를 리팩터링하기가 더 쉽습니다.
다음과 같은 뚱뚱한 컴포넌트를 고려해 보겠습니다.
// views/Login.js
// 피해야 할 패턴
var Login = {
username: '',
password: '',
setUsername: function (value) {
this.username = value;
},
setPassword: function (value) {
this.password = value;
},
canSubmit: function () {
return this.username !== '' && this.password !== '';
},
login: function () {
/*...*/
},
view: function () {
return m('.login', [
m('input[type=text]', {
oninput: function (e) {
this.setUsername(e.target.value);
},
value: this.username,
}),
m('input[type=password]', {
oninput: function (e) {
this.setPassword(e.target.value);
},
value: this.password,
}),
m(
'button',
{ disabled: !this.canSubmit(), onclick: this.login },
'Login'
),
]);
},
};
일반적으로 더 큰 애플리케이션의 컨텍스트에서 위의 로그인 컴포넌트는 사용자 등록 및 암호 복구를 위한 컴포넌트와 함께 존재합니다. 로그인 화면에서 등록 또는 암호 복구 화면으로 이동할 때 이메일 필드를 미리 채워 사용자가 잘못된 페이지를 채운 경우 이메일을 다시 입력할 필요가 없도록 하거나 사용자 이름을 찾을 수 없는 경우 사용자를 등록 양식으로 이동할 수 있다고 상상해 보십시오.
이 컴포넌트에서 username
과 password
필드를 다른 컴포넌트와 공유하기 어렵다는 것을 알 수 있습니다. 이는 뚱뚱한 컴포넌트가 상태를 캡슐화하기 때문이며, 이는 외부에서의 접근을 어렵게 만듭니다.
이 컴포넌트를 리팩터링하고 상태 코드를 컴포넌트에서 애플리케이션의 데이터 레이어로 옮기는 것이 더 합리적입니다. 이는 새 모듈을 만드는 것만큼 간단할 수 있습니다.
// models/Auth.js
// 권장되는 패턴
var Auth = {
username: '',
password: '',
setUsername: function (value) {
Auth.username = value;
},
setPassword: function (value) {
Auth.password = value;
},
canSubmit: function () {
return Auth.username !== '' && Auth.password !== '';
},
login: function () {
/*...*/
},
};
module.exports = Auth;
그런 다음 컴포넌트를 정리할 수 있습니다.
// views/Login.js
// 권장되는 패턴
var Auth = require('../models/Auth');
var Login = {
view: function () {
return m('.login', [
m('input[type=text]', {
oninput: function (e) {
Auth.setUsername(e.target.value);
},
value: Auth.username,
}),
m('input[type=password]', {
oninput: function (e) {
Auth.setPassword(e.target.value);
},
value: Auth.password,
}),
m(
'button',
{
disabled: !Auth.canSubmit(),
onclick: Auth.login,
},
'Login'
),
]);
},
};
이러한 방식으로 Auth
모듈은 이제 인증 관련 상태의 중앙 관리소가 되며 Register
컴포넌트는 이 데이터에 쉽게 접근할 수 있으며 필요한 경우 canSubmit
과 같은 메서드를 재사용할 수도 있습니다. 또한 (예: 이메일 필드의 경우) 유효성 검사 코드가 필요한 경우 setEmail
만 수정하면 되며 해당 변경 사항은 이메일 필드를 수정하는 모든 컴포넌트에 대해 이메일 유효성 검사를 수행합니다.
추가적으로 컴포넌트의 이벤트 핸들러에 대한 상태 참조를 유지하기 위해 더 이상 .bind
를 사용할 필요가 없습니다.
vnode.attrs
자체를 다른 vnode로 전달하지 마십시오
인터페이스의 유연성을 유지하고 구현을 단순화하기 위해, 특정 속성을 자식 컴포넌트나 요소로 전달해야 할 때가 있습니다. 예를 들어 Bootstrap의 모달이 있습니다. 다음과 같이 vnode의 속성을 전달하고 싶을 수 있습니다.
// 피해야 할 패턴
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs, [
// 여기에서 `vnode.attrs` 전달 ^
// ...
]);
},
};
위와 같이 하면 사용할 때 문제가 발생할 수 있습니다.
var MyModal = {
view: function () {
return m(
Modal,
{
// 두 번 토글하므로 표시되지 않음
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
[
// ...
]
);
},
};
대신 단일 속성을 vnode로 전달해야 합니다.
// 권장되는 패턴
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs.attrs, [
// 여기에서 `attrs:` 전달 ^
// ...
]);
},
};
// 예시
var MyModal = {
view: function () {
return m(Modal, {
attrs: {
// 한 번 토글
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
// ...
});
},
};
children
을 조작하지 마십시오
컴포넌트가 속성이나 자식을 적용하는 방식에 특정한 규칙이 있다면, 사용자 정의 속성을 사용하는 것이 좋습니다.
종종 컴포넌트에 구성 가능한 제목과 본문이 있는 경우와 같이 여러 자식 집합을 정의하는 것이 바람직합니다.
이러한 목적으로 children
속성을 분해하지 마십시오.
// 피해야 할 패턴
var Header = {
view: function (vnode) {
return m('.section', [
m('.header', vnode.children[0]),
m('.tagline', vnode.children[1]),
]);
},
};
m(Header, [m('h1', 'My title'), m('h2', 'Lorem ipsum')]);
// 부자연스러운 사용 사례
m(Header, [
[m('h1', 'My title'), m('small', 'A small note')],
m('h2', 'Lorem ipsum'),
]);
위의 컴포넌트는 자식이 수신된 것과 동일한 연속 형식으로 출력된다는 가정을 깨뜨립니다. 구현을 읽지 않고는 컴포넌트를 이해하기 어렵습니다. 대신 속성을 명명된 매개변수로 사용하고 children
을 균일한 자식 콘텐츠에 예약하십시오.
// 권장되는 패턴
var BetterHeader = {
view: function (vnode) {
return m('.section', [
m('.header', vnode.attrs.title),
m('.tagline', vnode.attrs.tagline),
]);
},
};
m(BetterHeader, {
title: m('h1', 'My title'),
tagline: m('h2', 'Lorem ipsum'),
});
// 더 명확한 사용 사례
m(BetterHeader, {
title: [m('h1', 'My title'), m('small', 'A small note')],
tagline: m('h2', 'Lorem ipsum'),
});
컴포넌트를 정적으로 정의하고 동적으로 호출하십시오
뷰 내에서 컴포넌트 정의를 만들지 마십시오
view
메서드 내에서 (직접 인라인으로 또는 그렇게 하는 함수를 호출하여) 컴포넌트를 만드는 경우 각 리렌더링은 컴포넌트의 다른 사본을 갖습니다. 컴포넌트 vnode를 diff할 때 새 vnode에서 참조하는 컴포넌트가 이전 컴포넌트에서 참조하는 컴포넌트와 엄격하게 동일하지 않으면 둘 다 궁극적으로 동일한 코드를 실행하더라도 서로 다른 컴포넌트로 간주됩니다. 다시 말해, 생성 함수를 통해 동적으로 생성되는 컴포넌트는 매번 처음부터 다시 생성됩니다.
이러한 이유로 컴포넌트를 다시 만들지 않아야 합니다. 대신 일반적인 방식으로 컴포넌트를 사용하십시오.
// 피해야 할 패턴
var ComponentFactory = function (greeting) {
// 모든 호출에서 새 컴포넌트 생성
return {
view: function () {
return m('div', greeting);
},
};
};
m.render(document.body, m(ComponentFactory('hello')));
// 두 번째로 호출하면 아무것도 하지 않고 처음부터 div를 다시 생성합니다.
m.render(document.body, m(ComponentFactory('hello')));
// 권장되는 패턴
var Component = {
view: function (vnode) {
return m('div', vnode.attrs.greeting);
},
};
m.render(document.body, m(Component, { greeting: 'hello' }));
// 두 번째로 호출해도 DOM이 수정되지 않습니다.
m.render(document.body, m(Component, { greeting: 'hello' }));
뷰 외부에서 컴포넌트 인스턴스를 만들지 마십시오
반대로 유사한 이유로 컴포넌트 인스턴스가 뷰 외부에서 생성되면 향후 다시 그리기는 노드 간 동일성 검사를 수행하고 건너뜁니다. 따라서 컴포넌트 인스턴스는 항상 뷰 내에서 생성되어야 합니다.
// 피해야 할 패턴
var Counter = {
count: 0,
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + vnode.state.count),
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'Increase count'
)
);
},
};
var counter = m(Counter);
m.mount(document.body, {
view: function (vnode) {
return [m('h1', 'My app'), counter];
},
});
위 예시에서 카운터 컴포넌트의 버튼을 클릭하면 상태 카운트가 증가하지만, 컴포넌트를 나타내는 vnode가 동일한 참조를 공유하기 때문에 뷰가 갱신되지 않아 렌더링 과정에서 변경 사항을 감지하지 못합니다. 새 vnode가 생성되도록 항상 뷰에서 컴포넌트를 호출해야 합니다.
// 권장되는 패턴
var Counter = {
count: 0,
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + vnode.state.count),
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'Increase count'
)
);
},
};
m.mount(document.body, {
view: function (vnode) {
return [m('h1', 'My app'), m(Counter)];
},
});