route(root, defaultRoute, routes)
Description
애플리케이션 내에서 페이지 간의 이동을 가능하게 합니다.
var Home = {
view: function () {
return 'Welcome';
},
};
m.route(document.body, '/home', {
'/home': Home, // `https://localhost/#!/home` 으로 정의
});
애플리케이션당 m.route
는 한 번만 호출해야 합니다.
Signature
m.route(root, defaultRoute, routes)
인수 | 타입 | 필수 | 설명 |
---|---|---|---|
root | Element | 예 | 서브 트리의 부모 노드가 될 DOM 엘리먼트 |
defaultRoute | String | 예 | 현재 URL이 어떤 경로와도 일치하지 않을 경우 리디렉션할 경로입니다. 초기 경로와는 다릅니다. |
routes | Object<String,Component|RouteResolver> | 예 | 경로 문자열을 키로, 컴포넌트 또는 RouteResolver를 값으로 가지는 객체 |
반환 값 | undefined 반환 |
Static members
m.route.set
일치하는 경로로 리디렉션하거나, 일치하는 경로를 찾을 수 없는 경우 기본 경로로 리디렉션합니다. 모든 마운트 지점에서 비동기 리드로우를 트리거합니다.
m.route.set(path, params, options)
인수 | 타입 | 필수 | 설명 |
---|---|---|---|
path | String | 예 | 접두사가 없는 라우팅할 경로 이름입니다. 경로는 params 의 값으로 보간된 파라미터를 포함할 수 있습니다. |
params | Object | 아니요 | 라우팅 파라미터입니다. path 에 라우팅 파라미터 슬롯이 있는 경우, 이 객체의 속성이 경로 문자열로 보간됩니다. |
options.replace | Boolean | 아니요 | 새로운 히스토리 항목을 만들지, 현재 항목을 대체할지 여부입니다. 기본값은 false 입니다. |
options.state | Object | 아니요 | 기본 history.pushState / history.replaceState 호출에 전달할 state 객체입니다. 이 state 객체는 history.state 속성에서 사용할 수 있으며, 라우팅 파라미터 객체와 병합됩니다. 이 옵션은 pushState API를 사용할 때만 작동하며, 라우터가 hashchange 모드로 폴백하는 경우(즉, pushState API를 사용할 수 없는 경우) 무시됩니다. |
options.title | String | 아니요 | 기본 history.pushState / history.replaceState 호출에 전달할 title 문자열입니다. |
반환 값 | undefined 반환 |
.set
을 params
와 함께 사용하려면 반드시 경로를 정의해야 합니다.
var Article = {
view: function (vnode) {
return 'This is article ' + vnode.attrs.articleid;
},
};
m.route(document.body, {
'/article/:articleid': Article,
});
m.route.set('/article/:articleid', { articleid: 1 });
m.route.get
접두사 없이 마지막으로 완전히 확인된 라우팅 경로를 반환합니다. 비동기 경로가 해결 중일 때는 주소 표시줄에 보이는 경로와 다를 수 있습니다.
path = m.route.get()
인수 | 타입 | 필수 | 설명 |
---|---|---|---|
반환 값 | String | 마지막으로 완전히 확인된 경로를 반환합니다. |
m.route.prefix
라우터 접두사를 정의합니다. 라우터 접두사는 라우터가 사용하는 기본 전략을 결정하는 URL의 일부입니다.
m.route.prefix = prefix
인수 | 타입 | 필수 | 설명 |
---|---|---|---|
prefix | String | 예 | Mithril이 사용하는 기본 라우팅 전략을 제어하는 접두사입니다. |
이 속성은 읽고 쓸 수 있습니다.
m.route.Link
이 컴포넌트는 동적으로 라우팅되는 링크를 생성합니다. 주요 기능은 로컬 href
가 경로 접두사를 반영하도록 변환된 <a>
링크를 생성하는 것입니다.
m(m.route.Link, { href: '/foo' }, 'foo');
// m.route.prefix가 기본 전략에서 변경되지 않은 경우 다음으로 렌더링됩니다.
// <a href="#!/foo">foo</a>
링크는 다음과 같은 특별한 속성들을 선택적으로 사용할 수 있습니다.
selector
는m
함수의 첫 번째 인수로 전달되는 값입니다.<a>
엘리먼트를 포함하여 모든 CSS 선택자가 유효합니다.params
&options
는m.route.set
에 정의된 것과 동일한 인수입니다.disabled
가true
이면 라우팅 동작과 바인딩된onclick
핸들러를 비활성화하고 접근성 힌트를 위해data-disabled="true"
속성을 추가합니다. 엘리먼트가a
인 경우href
가 제거됩니다.
라우팅 동작은 이벤트 처리 API를 사용하여 막을 수 없습니다. 대신 disabled
를 사용하십시오.
m(
m.route.Link,
{
href: '/foo',
selector: 'button.large',
disabled: true,
params: { key: 'value' },
options: { replace: true },
},
'link name'
);
// 다음으로 렌더링됩니다.
// <button disabled aria-disabled="true" class="large">link name</button>
vnode = m(m.route.Link, attributes, children)
인수 | 타입 | 필수 | 설명 |
---|---|---|---|
attributes.href | Object | 예 | 탐색할 대상 경로입니다. |
attributes.disabled | Boolean | 아니요 | 해당 엘리먼트의 접근성을 비활성화합니다. |
attributes.selector | String|Object|Function | 아니요 | m 에 대한 선택자입니다. 기본값은 "a" 입니다. |
attributes.options | Object | 아니요 | m.route.set 에 전달되는 options 를 설정합니다. |
attributes.params | Object | 아니요 | m.route.set 에 전달되는 params 를 설정합니다. |
attributes | Object | 아니요 | m 으로 전달될 다른 속성입니다. |
children | Array<Vnode>|String|Number|Boolean | 아니요 | 이 링크에 대한 자식 vnode입니다. |
반환 값 | Vnode | vnode입니다. |
m.route.param
마지막으로 완전히 확인된 경로에서 경로 파라미터를 검색합니다. 경로 파라미터는 키-값 쌍입니다. 경로 파라미터는 다음 위치에서 가져올 수 있습니다.
- 경로 보간 (예: 경로가
/users/:id
이고/users/1
로 확인되면 경로 파라미터는 키id
와 값"1"
을 가짐) - 라우터 쿼리 문자열 (예: 경로가
/users?page=1
이면 경로 파라미터는 키page
와 값"1"
을 가짐) history.state
(예:history.state
가{foo: "bar"}
이면 경로 파라미터는 키foo
와 값"bar"
을 가짐)
value = m.route.param(key)
인수 | 타입 | 필수 | 설명 |
---|---|---|---|
key | String | 아니요 | 경로 파라미터 이름 (예: 경로 /users/:id 의 id , 또는 경로 /users/1?page=3 의 page , 또는 history.state 의 키) |
반환 값 | String|Object | 지정된 키에 대한 값을 반환합니다. 키가 지정되지 않은 경우 모든 보간된 키를 포함하는 객체를 반환합니다. |
RouteResolver
의 onmatch
함수에서 새 경로는 아직 완전히 확인되지 않았으며 m.route.param()
은 이전 경로의 파라미터를 반환합니다(있는 경우). onmatch
는 새 경로의 파라미터를 인수로 받습니다.
m.route.SKIP
다음 경로로 건너뛰기 위해 경로 해결사의 onmatch
에서 반환할 수 있는 특수 값입니다.
RouteResolver
RouteResolver
는 onmatch
메서드 및/또는 render
메서드를 포함하는 컴포넌트가 아닌 객체입니다. 두 메서드는 선택 사항이지만 하나 이상은 있어야 합니다.
객체가 컴포넌트로 인식될 수 있는 경우(view
메서드가 있거나 function
/class
인 경우) onmatch
또는 render
메서드가 있더라도 컴포넌트로 처리됩니다. RouteResolver
는 컴포넌트가 아니므로 라이프사이클 메서드가 없습니다.
일반적으로 RouteResolver
는 m.route
호출과 동일한 파일에 있어야 하고, 컴포넌트 정의는 자체 모듈에 있어야 합니다.
routeResolver = {onmatch, render}
컴포넌트를 사용하는 경우, 컴포넌트가 Home
이라고 가정할 때, 이 RouteResolver
는 다음과 같이 syntactic sugar로 표현될 수 있습니다.
var routeResolver = {
onmatch: function () {
return Home;
},
render: function (vnode) {
return [vnode];
},
};
routeResolver.onmatch
onmatch
훅은 라우터가 렌더링할 컴포넌트를 찾아야 할 때 호출됩니다. 라우터 경로가 변경될 때마다 한 번씩 호출되지만, 같은 경로에서 리드로우가 발생할 때는 호출되지 않습니다. 컴포넌트가 초기화되기 전에 로직을 실행하는 데 사용할 수 있습니다(예: 인증 로직, 데이터 미리 로드, 리디렉션 분석 추적 등).
이 메서드를 사용하면 컴포넌트를 비동기적으로 정의할 수 있어 코드 분할과 비동기 모듈 로딩에 적합합니다. 컴포넌트를 비동기적으로 렌더링하려면 컴포넌트 인스턴스로 resolve되는 Promise
를 반환합니다.
onmatch
에 대한 자세한 내용은 고급 컴포넌트 해결 섹션을 참조하십시오.
routeResolver.onmatch(args, requestedPath, route)
인수 | 타입 | 설명 |
---|---|---|
args | Object | 라우팅 파라미터 |
requestedPath | String | 마지막 라우팅 작업에서 요청한 라우터 경로입니다. 보간된 라우팅 파라미터 값을 포함하지만 접두사는 제외합니다. onmatch 가 호출되면 이 경로에 대한 확인이 완료되지 않았으며 m.route.get() 은 여전히 이전 경로를 반환합니다. |
route | String | 마지막 라우팅 작업에서 요청한 라우터 경로입니다. 보간된 라우팅 파라미터 값을 제외합니다. |
반환 값 | Component|\Promise<Component>|undefined | 컴포넌트 또는 컴포넌트로 resolve되는 Promise 를 반환합니다. |
onmatch
가 컴포넌트 또는 컴포넌트로 resolve되는 Promise
를 반환하는 경우 이 컴포넌트는 RouteResolver
의 render
메서드에서 첫 번째 인수에 대한 vnode.tag
로 사용됩니다. 그렇지 않으면 vnode.tag
가 "div"
로 설정됩니다. 마찬가지로 onmatch
메서드가 생략되면 vnode.tag
도 "div"
입니다.
onmatch
가 거부되는 Promise
를 반환하는 경우 라우터는 defaultRoute
로 다시 리디렉션합니다. 반환하기 전에 Promise
체인에서 .catch
를 호출하여 이 동작을 재정의할 수 있습니다.
routeResolver.render
render
메서드는 일치하는 경로에 대한 모든 다시 그리기에 대해 호출됩니다. 컴포넌트의 view
메서드와 유사하며 컴포넌트 구성을 단순화하기 위해 존재합니다. 또한 Mithril.js가 서브트리 전체를 교체하는 기본 동작을 피할 수 있습니다.
vnode = routeResolver.render(vnode)
인수 | 타입 | 설명 |
---|---|---|
vnode | Object | 속성 객체에 라우팅 파라미터가 포함된 vnode입니다. onmatch 가 컴포넌트 또는 컴포넌트로 resolve되는 Promise 를 반환하지 않으면 vnode 의 tag 필드는 기본적으로 "div" 입니다. |
vnode.attrs | Object | URL 파라미터 값의 맵 |
반환 값 | Array<Vnode>|Vnode | 렌더링할 vnode |
vnode
파라미터는 m(Component, m.route.param())
입니다. 여기서 Component
는 경로에 대해 확인된 컴포넌트이고(routeResolver.onmatch
이후) m.route.param()
은 여기에 설명되어 있습니다. 이 메서드를 생략하면 기본 반환 값은 [vnode]
이며, key 파라미터를 사용할 수 있도록 fragment로 래핑됩니다. :key
파라미터와 결합하면 단일 엘리먼트 key가 지정된 fragment가 됩니다. 왜냐하면 [m(Component, {key: m.route.param("key"), ...})]
와 같이 렌더링되기 때문입니다.
How it works
라우팅은 SPA(Single Page Application)를 구축하기 위한 시스템입니다. SPA는 전체 브라우저 새로고침 없이 애플리케이션 내에서 페이지 간 이동을 가능하게 합니다.
각 페이지를 개별적으로 북마크할 수 있는 기능과 브라우저의 히스토리 메커니즘을 통해 애플리케이션을 탐색할 수 있는 기능을 유지하면서 원활한 탐색 기능을 제공합니다.
페이지 새로 고침 없는 라우팅은 부분적으로 history.pushState
API에 의해 가능합니다. 이 API를 사용하면 페이지가 로드된 후 브라우저에 표시되는 URL을 프로그래밍 방식으로 변경할 수 있지만, 콜드 상태(예: 새 탭)에서 주어진 URL로 이동하면 적절한 마크업이 렌더링되도록 하는 것은 애플리케이션 개발자의 책임입니다.
Routing strategies
라우팅 전략은 라이브러리가 실제로 라우팅을 구현하는 방법을 결정합니다. SPA 라우팅 시스템을 구현하는 데 사용할 수 있는 세 가지 일반적인 전략이 있으며, 각 전략에는 다른 고려 사항이 있습니다.
m.route.prefix = '#!'
(기본값) – URL의 fragment identifier(해시라고도 함) 부분을 사용합니다. 이 전략을 사용하는 URL은 일반적으로https://localhost/#!/page1
과 같습니다.m.route.prefix = '?'
– 쿼리 문자열을 사용합니다. 이 전략을 사용하는 URL은 일반적으로https://localhost/?/page1
과 같습니다.m.route.prefix = ''
– 경로 이름을 사용합니다. 이 전략을 사용하는 URL은 일반적으로https://localhost/page1
과 같습니다.
해시 전략을 사용하면 history.pushState
를 지원하지 않는 브라우저에서 작동하는 것이 보장됩니다. onhashchange
이벤트 핸들러를 폴백 메커니즘으로 사용할 수 있기 때문입니다. 해시를 순전히 로컬로 유지하려면 이 전략을 사용하십시오.
쿼리 문자열 전략을 사용하면 서버 측 감지가 가능하지만 일반적인 경로로 표시되지는 않습니다. 서버 측에서 앵커 링크를 지원하거나 감지해야 하지만, 경로 이름 전략을 사용하기 위한 서버 설정을 변경할 수 없는 경우 (예: Apache 서버에서 .htaccess
파일 수정이 불가능한 경우) 이 전략을 사용하십시오.
경로 이름 전략은 가장 깔끔한 URL을 생성하지만 애플리케이션이 라우팅할 수 있는 모든 URL에서 단일 페이지 애플리케이션 코드를 제공하도록 서버를 설정해야 합니다. 더 깔끔한 URL을 원하면 이 전략을 사용하십시오.
해시 전략을 사용하는 단일 페이지 애플리케이션은 종종 해시 뒤에 느낌표를 사용하여 해시를 앵커에 연결하는 목적이 아닌 라우팅 메커니즘으로 사용하고 있음을 나타내는 규칙을 사용합니다. #!
문자열은 hashbang이라고 합니다.
기본 전략은 해시뱅을 사용합니다.
Typical usage
일반적으로 경로에 매핑할 몇 개의 컴포넌트를 생성해야 합니다.
var Home = {
view: function () {
return [m(Menu), m('h1', 'Home')];
},
};
var Page1 = {
view: function () {
return [m(Menu), m('h1', 'Page 1')];
},
};
위의 예에서는 Home
과 Page1
이라는 두 개의 컴포넌트가 있습니다. 각 컴포넌트에는 메뉴와 일부 텍스트가 포함되어 있습니다. 메뉴 자체는 반복을 피하기 위해 컴포넌트로 정의됩니다.
var Menu = {
view: function () {
return m('nav', [
m(m.route.Link, { href: '/' }, 'Home'),
m(m.route.Link, { href: '/page1' }, 'Page 1'),
]);
},
};
이제 경로를 정의하고 컴포넌트를 매핑할 수 있습니다.
m.route(document.body, '/', {
'/': Home,
'/page1': Page1,
});
여기서는 두 개의 경로인 /
와 /page1
을 지정합니다. 사용자가 각 URL로 이동할 때 해당 컴포넌트를 렌더링합니다.
Navigating to different routes
위의 예에서 Menu
컴포넌트에는 두 개의 m.route.Link
가 있습니다. 이는 엘리먼트(기본적으로 <a>
)를 만들고 사용자가 클릭하면 현재 애플리케이션 내에서 다른 경로로 이동하도록 설정합니다. 원격 서버로 이동하는 것이 아닙니다.
m.route.set(route)
를 통해 프로그래밍 방식으로 이동할 수도 있습니다. 예를 들어 m.route.set("/page1")
과 같습니다.
경로 간을 탐색할 때 라우터 접두사가 처리됩니다. 즉, Mithril.js 경로를 연결할 때 m.route.set
과 m.route.Link
모두에서 해시뱅 #!
(또는 m.route.prefix
를 설정한 접두사)을 생략하십시오.
컴포넌트 간을 탐색할 때 전체 서브트리가 대체됩니다. 서브트리만 패치하려면 render
메서드가 있는 경로 해결사를 사용하십시오.
Routing parameters
경우에 따라 변수 ID 또는 유사한 데이터를 경로에 표시하고 싶지만 가능한 모든 ID에 대해 별도의 경로를 명시적으로 지정하고 싶지 않습니다. 이를 위해 Mithril.js는 파라미터화된 경로를 지원합니다.
var Edit = {
view: function (vnode) {
return [m(Menu), m('h1', 'Editing ' + vnode.attrs.id)];
},
};
m.route(document.body, '/edit/1', {
'/edit/:id': Edit,
});
위의 예에서는 경로 /edit/:id
를 정의했습니다. 이는 /edit/
로 시작하고 일부 데이터(예: /edit/1
, edit/234
등)가 뒤따르는 모든 URL과 일치하는 동적 경로를 만듭니다. 그런 다음 id
값은 컴포넌트의 vnode의 속성으로 매핑됩니다(vnode.attrs.id
).
경로에 여러 인수를 가질 수 있습니다. 예를 들어 /edit/:projectID/:userID
는 컴포넌트의 vnode 속성 객체에 projectID
및 userID
속성을 생성합니다.
Key parameter
사용자가 파라미터화된 경로에서 다른 파라미터가 있는 동일한 경로로 이동할 때(예: 경로 /page/:id
가 주어졌을 때 /page/1
에서 /page/2
로 이동) 두 경로가 동일한 컴포넌트로 확인되므로 컴포넌트가 처음부터 다시 생성되지 않고 가상 DOM이 제자리에서 비교(diff)됩니다. 이로 인해 컴포넌트가 처음 생성될 때 호출되는 oninit
또는 oncreate
라이프사이클 훅 대신, 업데이트 시 호출되는 onupdate
훅이 실행되는 결과가 발생합니다. 그러나 개발자가 컴포넌트의 재생성을 경로 변경 이벤트와 동기화하고 싶어하는 것은 비교적 일반적입니다.
이를 위해 경로 파라미터화를 key와 결합하여 매우 편리한 패턴을 만들 수 있습니다.
m.route(document.body, '/edit/1', {
'/edit/:key': Edit,
});
이는 경로의 루트 컴포넌트에 대해 생성된 vnode에 경로 파라미터 객체 key
가 있음을 의미합니다. 경로 파라미터는 vnode의 attrs
속성으로 전달됩니다. 따라서 한 페이지에서 다른 페이지로 이동할 때 key
가 변경되어 컴포넌트가 처음부터 다시 생성됩니다(key는 가상 DOM 엔진에 이전 컴포넌트와 새 컴포넌트가 다른 엔터티임을 알려주기 때문).
이 아이디어를 더 발전시켜 다시 로드될 때 자체적으로 다시 생성되는 컴포넌트를 만들 수 있습니다.
m.route.set(m.route.get(), {key: Date.now()})
또는 history state
기능을 사용하여 URL을 변경하지 않고 다시 로드할 수 있는 컴포넌트를 만들 수도 있습니다.
m.route.set(m.route.get(), null, {state: {key: Date.now()}})
key 파라미터는 컴포넌트 경로에만 작동합니다. 경로 해결사를 사용하는 경우 key: m.route.param("key")
를 전달하여 단일 자식 key가 지정된 fragment를 사용하여 동일한 작업을 수행해야 합니다.
Variadic routes
가변 경로, 즉 슬래시가 포함된 URL 경로 이름을 포함하는 인수가 있는 경로를 가질 수도 있습니다.
m.route(document.body, '/edit/pictures/image.jpg', {
'/edit/:file...': Edit,
});
Handling 404s
Isomorphic/Universal JavaScript 애플리케이션에서, URL 파라미터와 가변 경로를 함께 사용하면 사용자 정의 404 오류 페이지를 표시하는 데 유용합니다.
404 Not Found 오류의 경우 서버는 사용자 지정 페이지를 클라이언트로 다시 보냅니다. Mithril.js가 로드되면 해당 경로를 알 수 없으므로 클라이언트를 기본 경로로 리디렉션합니다.
m.route(document.body, '/', {
'/': homeComponent,
// [...]
'/:404...': errorPageComponent,
});
History state
기본적으로 제공되는 history.pushState
API를 활용하여 사용자 탐색 경험을 향상시킬 수 있습니다. 예를 들어 애플리케이션은 사용자가 페이지를 떠날 때 큰 양식의 상태를 "기억"하여 사용자가 브라우저에서 뒤로 버튼을 누르면 빈 양식이 아닌 양식이 채워지도록 할 수 있습니다.
예를 들어 다음과 같은 양식을 만들 수 있습니다.
var state = {
term: '',
search: function () {
// save the state for this route
// this is equivalent to `history.replaceState({term: state.term}, null, location.href)`
m.route.set(m.route.get(), null, {
replace: true,
state: { term: state.term },
});
// navigate away
location.href = 'https://google.com/?q=' + state.term;
},
};
var Form = {
oninit: function (vnode) {
state.term = vnode.attrs.term || ''; // populated from the `history.state` property if the user presses the back button
},
view: function () {
return m('form', [
m("input[placeholder='Search']", {
oninput: function (e) {
state.term = e.target.value;
},
value: state.term,
}),
m('button', { onclick: state.search }, 'Search'),
]);
},
};
m.route(document.body, '/', {
'/': Form,
});
이렇게 하면 사용자가 검색 후 뒤로 버튼을 눌러 애플리케이션으로 돌아와도 입력란이 검색어로 채워진 상태를 유지합니다. 사용자가 직접 입력하기 번거로운 비-영구적인 상태를 가지는 대규모 폼이나 다른 애플리케이션의 사용자 경험을 개선할 수 있습니다.
Changing router prefix
라우터 접두사는 라우터가 사용하는 기본 전략을 결정하는 URL의 일부입니다.
// pathname 전략으로 설정
m.route.prefix = '';
// querystring 전략으로 설정
m.route.prefix = '?';
// 해시뱅 없는 해시 전략으로 설정
m.route.prefix = '#';
// 루트 URL이 아닌 하위 URL에서 pathname 전략을 사용할 때
// 예: 앱이 `https://localhost/my-app` 아래에 있고 다른 것이
// `https://localhost` 아래에 있는 경우
m.route.prefix = '/my-app';
고급 컴포넌트 처리
컴포넌트를 경로에 매핑하는 대신 RouteResolver
객체를 지정할 수 있습니다. RouteResolver
객체는 onmatch()
및/또는 render()
메서드를 포함합니다. 두 메서드는 선택 사항이지만, 적어도 하나는 반드시 존재해야 합니다.
m.route(document.body, '/', {
'/': {
onmatch: function (args, requestedPath, route) {
return Home;
},
render: function (vnode) {
return vnode; // m(Home)과 동일합니다.
},
},
});
RouteResolver
는 다양한 고급 라우팅 시나리오를 구현하는 데 유용합니다.
레이아웃 컴포넌트 감싸기
라우팅된 컴포넌트의 전부 또는 대부분을 재사용 가능한 틀(일반적으로 "레이아웃"이라고 함)로 감싸는 것이 좋습니다. 이를 위해 먼저 다양한 컴포넌트를 감싸는 공통 마크업을 포함하는 컴포넌트를 만들어야 합니다.
var Layout = {
view: function (vnode) {
return m('.layout', vnode.children);
},
};
위 예제에서 레이아웃은 컴포넌트에 전달된 자식을 포함하는 <div class="layout">
으로 구성되지만, 실제 시나리오에서는 필요에 따라 더 복잡하게 구성할 수 있습니다.
레이아웃을 감싸는 한 가지 방법은 경로 맵에서 익명 컴포넌트를 정의하는 것입니다.
// 예시 1
m.route(document.body, '/', {
'/': {
view: function () {
return m(Layout, m(Home));
},
},
'/form': {
view: function () {
return m(Layout, m(Form));
},
},
});
그러나 최상위 컴포넌트가 익명 컴포넌트이기 때문에 /
경로에서 /form
경로로 (또는 그 반대로) 이동하면 익명 컴포넌트가 해체되고 DOM이 처음부터 다시 생성됩니다. Layout
컴포넌트에 라이프사이클 메서드가 정의되어 있다면 oninit
및 oncreate
훅이 모든 경로 변경 시 실행됩니다. 애플리케이션에 따라 이것이 바람직할 수도 있고 그렇지 않을 수도 있습니다.
Layout
컴포넌트가 처음부터 다시 생성되지 않고, diff 알고리즘에 의해 변경 사항만 반영되도록 하려면 RouteResolver
를 루트 객체로 사용해야 합니다.
// 예시 2
m.route(document.body, '/', {
'/': {
render: function () {
return m(Layout, m(Home));
},
},
'/form': {
render: function () {
return m(Layout, m(Form));
},
},
});
이 경우 Layout
컴포넌트에 oninit
및 oncreate
라이프사이클 메서드가 있다면 첫 번째 경로 변경 시에만 실행됩니다 (모든 경로가 동일한 레이아웃을 사용한다고 가정).
두 예시의 차이점을 명확히 하기 위해 예시 1은 다음 코드와 같습니다.
// 예시 1과 기능적으로 동일
var Anon1 = {
view: function () {
return m(Layout, m(Home));
},
};
var Anon2 = {
view: function () {
return m(Layout, m(Form));
},
};
m.route(document.body, '/', {
'/': {
render: function () {
return m(Anon1);
},
},
'/form': {
render: function () {
return m(Anon2);
},
},
});
Anon1
과 Anon2
는 서로 다른 컴포넌트이므로 해당 하위 트리 (Layout 포함)가 처음부터 다시 생성됩니다. 이는 컴포넌트가 RouteResolver
없이 직접 사용될 때도 발생합니다.
예시 2에서 Layout
은 두 경로 모두에서 최상위 컴포넌트이므로 Layout
컴포넌트의 DOM이 비교(diff)됩니다 (즉, 변경 사항이 없으면 그대로 유지됨). Home
에서 Form
으로의 변경만 해당 DOM 섹션의 재생성을 트리거합니다.
경로 재지정
RouteResolver
의 onmatch
훅은 경로의 최상위 컴포넌트가 초기화되기 전에 로직을 실행하는 데 사용할 수 있습니다. Mithril의 m.route.set()
또는 네이티브 HTML의 history
API를 사용할 수 있습니다. history
API를 사용하여 리디렉션할 때, onmatch
훅은 일치하는 경로의 처리가 완료되지 않도록 영원히 resolve 되지 않는 Promise를 반환해야 합니다. m.route.set()
은 내부적으로 해당 경로의 처리를 취소하므로, 별도로 Promise를 반환할 필요가 없습니다.
예시: 인증
아래 예는 사용자가 로그인하지 않으면 /secret
페이지를 볼 수 없도록 하는 로그인 장벽을 구현하는 방법을 보여줍니다.
var isLoggedIn = false;
var Login = {
view: function () {
return m('form', [
m(
'button[type=button]',
{
onclick: function () {
isLoggedIn = true;
m.route.set('/secret');
},
},
'Login'
),
]);
},
};
m.route(document.body, '/secret', {
'/secret': {
onmatch: function () {
if (!isLoggedIn) m.route.set('/login');
else return Home;
},
},
'/login': Login,
});
애플리케이션이 로드되면 onmatch
가 호출되고 isLoggedIn
이 false이므로 애플리케이션은 /login
으로 리디렉션됩니다. 사용자가 로그인 버튼을 누르면 isLoggedIn
이 true로 설정되고 애플리케이션은 /secret
으로 리디렉션됩니다. onmatch
훅이 다시 실행되고 이번에는 isLoggedIn
이 true이므로 애플리케이션은 Home
컴포넌트를 렌더링합니다.
위 예제에서는 단순함을 위해 사용자의 로그인 상태를 전역 변수에 저장하고, 로그인 버튼 클릭 시 해당 변수의 값을 변경하는 방식으로 구현했습니다. 실제 애플리케이션에서는 사용자가 올바른 로그인 정보를 입력해야 하며, 로그인 버튼을 클릭하면 서버에 인증 요청을 보내 사용자를 인증하는 과정을 거칩니다.
var Auth = {
username: '',
password: '',
setUsername: function (value) {
Auth.username = value;
},
setPassword: function (value) {
Auth.password = value;
},
login: function () {
m.request({
url: '/api/v1/auth',
params: { username: Auth.username, password: Auth.password },
}).then(function (data) {
localStorage.setItem('auth-token', data.token);
m.route.set('/secret');
});
},
};
var Login = {
view: function () {
return m('form', [
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[type=button]', { onclick: Auth.login }, 'Login'),
]);
},
};
m.route(document.body, '/secret', {
'/secret': {
onmatch: function () {
if (!localStorage.getItem('auth-token')) m.route.set('/login');
else return Home;
},
},
'/login': Login,
});
데이터 미리 로드
일반적으로 컴포넌트는 초기화될 때 데이터를 로드할 수 있습니다. 이런 방식으로 데이터를 로드하면 컴포넌트가 초기 렌더링과 데이터 로드 후 렌더링, 총 두 번 렌더링됩니다. loadUsers()
가 Promise를 반환하지만 oninit
에서 반환된 Promise는 현재 무시됩니다. 두 번째 렌더링은 m.request
의 background
옵션에서 비롯됩니다.
var state = {
users: [],
loadUsers: function () {
return m.request('/api/v1/users').then(function (users) {
state.users = users;
});
},
};
m.route(document.body, '/user/list', {
'/user/list': {
oninit: state.loadUsers,
view: function () {
return state.users.length > 0
? state.users.map(function (user) {
return m('div', user.id);
})
: 'loading';
},
},
});
위 예제에서 첫 번째 렌더링 시에는 요청이 완료되기 전에 state.users
가 빈 배열이므로 UI에 "loading"
이 표시됩니다. 그런 다음 데이터가 사용 가능해지면 UI가 다시 그려지고 사용자 ID 목록이 표시됩니다.
RouteResolver
를 사용하면 컴포넌트 렌더링 전에 데이터를 미리 로드하여 화면 깜빡임 현상을 줄이고, 로딩 표시기를 표시할 필요성을 없앨 수 있습니다.
var state = {
users: [],
loadUsers: function () {
return m.request('/api/v1/users').then(function (users) {
state.users = users;
});
},
};
m.route(document.body, '/user/list', {
'/user/list': {
onmatch: state.loadUsers,
render: function () {
return state.users.map(function (user) {
return m('div', user.id);
});
},
},
});
위 코드에서 render
는 요청이 완료된 후에만 실행되므로 삼항 연산자는 불필요합니다.
코드 분할
대규모 애플리케이션에서는 모든 경로에 대한 코드를 미리 로드하는 대신, 필요할 때 해당 경로의 코드만 다운로드하는 것이 효율적일 수 있습니다. 이러한 방식으로 코드베이스를 나누는 것을 코드 분할 또는 지연 로딩이라고 합니다. Mithril.js에서는 onmatch
훅에서 promise를 반환하여 이를 수행할 수 있습니다.
가장 기본적인 형태는 다음과 같습니다.
// Home.js
module.export = {
view: function () {
return [m(Menu), m('h1', 'Home')];
},
};
// index.js
function load(file) {
return m.request({
method: 'GET',
url: file,
extract: function (xhr) {
return new Function(
'var module = {};' + xhr.responseText + ';return module.exports;'
);
},
});
}
m.route(document.body, '/', {
'/': {
onmatch: function () {
return load('Home.js');
},
},
});
하지만 실제로 프로덕션 환경에서 사용하려면, Home.js
모듈과 관련된 모든 종속성을 서버에서 제공하는 하나의 파일로 묶어야 합니다 (번들링).
다행히 지연 로딩을 위해 모듈을 번들링하는 작업을 용이하게 하는 여러 도구가 있습니다. 다음은 많은 번들러에서 지원하는 네이티브 동적 import(...)
를 사용하는 예입니다.
m.route(document.body, '/', {
'/': {
onmatch: function () {
return import('./Home.js');
},
},
});
유형화된 경로
특정 고급 라우팅 시나리오에서는 경로 자체뿐만 아니라, 숫자 ID와 같은 특정 형식의 값만 매칭되도록 제한하고 싶을 수 있습니다. 경로에서 m.route.SKIP
을 반환하여 매우 쉽게 수행할 수 있습니다.
m.route(document.body, '/', {
'/view/:id': {
onmatch: function (args) {
if (!/^\d+$/.test(args.id)) return m.route.SKIP;
return ItemView;
},
},
'/view/:name': UserView,
});
숨겨진 경로
드문 경우지만 일부 사용자에 대해서는 특정 경로를 숨기고 다른 사용자에 대해서는 숨기지 않을 수 있습니다. 예를 들어, 특정 사용자가 다른 사용자의 정보를 볼 권한이 없는 경우, 권한 오류를 보여주는 대신 해당 경로가 존재하지 않는 것처럼 처리하고 404 페이지로 리디렉션할 수 있습니다. 이 경우 m.route.SKIP
을 사용하여 경로가 존재하지 않는 것처럼 처리할 수 있습니다.
m.route(document.body, '/', {
'/user/:id': {
onmatch: function (args) {
return Model.checkViewable(args.id).then(function (viewable) {
return viewable ? UserView : m.route.SKIP;
});
},
},
'/:404...': PageNotFound,
});
경로 취소/차단
RouteResolver
의 onmatch
는 절대 해결되지 않는 promise를 반환하여 경로 해결을 방지할 수 있습니다. 이는 중복된 경로 요청이 발생했을 때, 해당 요청을 감지하고 취소하는 데 사용할 수 있습니다.
m.route(document.body, '/', {
'/': {
onmatch: function (args, requestedPath) {
if (m.route.get() === requestedPath) return new Promise(function () {});
},
},
});
서드파티 통합
특정 상황에서는 React와 같은 다른 프레임워크와 연동해야 할 수 있습니다. 방법은 다음과 같습니다.
m.route
를 사용하여 모든 경로를 정상적으로 정의하되 한 번만 사용해야 합니다. 여러 경로 지점은 지원되지 않습니다.- 라우팅 구독을 해제해야 하는 경우,
m.mount(root, null)
을 호출하여 해제할 수 있습니다. 이때m.route(root, ...)
를 호출할 때 사용했던 동일한root
엘리먼트를 사용해야 합니다.m.route
는 내부적으로m.mount
를 사용하여 모든 것을 연결하므로 마법이 아닙니다.
다음은 React를 사용한 예입니다.
class Child extends React.Component {
constructor(props) {
super(props);
this.root = React.createRef();
}
componentDidMount() {
m.route(this.root, '/', {
// ...
});
}
componentDidUnmount() {
m.mount(this.root, null);
}
render() {
return <div ref={this.root} />;
}
}
다음은 Vue를 사용한 대략적인 동등물입니다.
<div ref="root"></div>
Vue.component('my-child', {
template: `<div ref="root"></div>`,
mounted: function () {
m.route(this.$refs.root, '/', {
// ...
});
},
destroyed: function () {
m.mount(this.$refs.root, null);
},
});