JSX
설명
JSX는 JavaScript 코드 내에서 HTML 태그를 직접 사용할 수 있도록 하는 JavaScript 구문 확장입니다. JSX는 JavaScript 표준의 일부는 아니지만, 애플리케이션 개발 시 선택적으로 사용할 수 있으며, 개인 또는 팀의 개발 스타일에 따라 유용할 수 있습니다.
function MyComponent() {
return {
view: () => m('main', [m('h1', 'Hello world')]),
};
}
// 다음과 같이 JSX로 작성할 수 있습니다.
function MyComponent() {
return {
view: () => (
<main>
<h1>Hello world</h1>
</main>
),
};
}
JSX 내에서 중괄호 {}
를 사용하여 JavaScript 표현식을 삽입할 수 있습니다.
var greeting = 'Hello';
var url = 'https://google.com';
var link = <a href={url}>{greeting}!</a>;
// 결과: <a href="https://google.com">Hello!</a>
컴포넌트 이름의 첫 글자를 대문자로 하거나 속성 접근 방식으로 사용할 수 있습니다.
m.render(document.body, <MyComponent />)
// m.render(document.body, m(MyComponent))와 동일합니다.
<m.route.Link href="/home">Go home</m.route.Link>
// m(m.route.Link, {href: "/home"}, "Go home")과 동일합니다.
설정
JSX를 사용하는 가장 쉬운 방법은 Babel 플러그인을 사용하는 것입니다.
Babel을 사용하려면 npm이 필요하며, npm은 Node.js 설치 시 함께 설치됩니다. npm이 설치되면 프로젝트 폴더를 생성하고 다음 명령을 실행합니다.
npm init -y
Webpack과 Babel을 함께 사용하려면 아래 섹션을 참고하세요.
Babel을 독립적인 도구로 설치하려면 다음 명령을 사용합니다.
npm install @babel/core @babel/cli @babel/preset-env @babel/plugin-transform-react-jsx --save-dev
.babelrc
파일을 생성합니다.
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"pragma": "m",
"pragmaFrag": "'['"
}
]
]
}
Babel을 실행하기 위해 npm 스크립트를 설정합니다. package.json
파일을 열고 "scripts"
아래에 다음 항목을 추가합니다.
{
"name": "my-project",
"scripts": {
"babel": "babel src --out-dir bin --source-maps"
}
}
이제 다음 명령어로 Babel을 실행할 수 있습니다.
npm run babel
Webpack과 함께 Babel 사용
Webpack을 아직 설치하지 않았다면 다음 명령을 사용하여 설치합니다.
npm install webpack webpack-cli --save-dev
다음 단계를 통해 Babel을 Webpack에 통합할 수 있습니다.
npm install @babel/core babel-loader @babel/preset-env @babel/plugin-transform-react-jsx --save-dev
.babelrc
파일을 생성합니다.
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"pragma": "m",
"pragmaFrag": "'['"
}
]
]
}
다음으로 webpack.config.js
파일을 생성합니다.
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './bin'),
filename: 'app.js',
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /\/node_modules\//,
use: {
loader: 'babel-loader',
},
},
],
},
resolve: {
extensions: ['.js', '.jsx'],
},
};
Webpack에 익숙한 사용자는 webpack.config.js
파일의 babel-loader
섹션에 Babel 옵션을 추가하면 오류가 발생할 수 있으므로, 별도의 .babelrc
파일에 옵션을 설정해야 합니다.
이 설정은 애플리케이션 진입점 파일이 src/index.js
에 있고, 번들 결과물이 bin/app.js
로 출력된다고 가정합니다.
번들러를 실행하기 위해 npm 스크립트를 설정합니다. package.json
파일을 열고 "scripts"
아래에 다음 항목을 추가합니다.
{
"name": "my-project",
"scripts": {
"start": "webpack --mode development --watch"
}
}
이제 명령줄에서 다음 명령어를 실행하여 번들러를 실행할 수 있습니다.
npm start
프로덕션용 빌드
최소화된 파일을 생성하려면 package.json
파일을 열고 build
라는 새로운 npm 스크립트를 추가합니다.
{
"name": "my-project",
"scripts": {
"start": "webpack -d --watch",
"build": "webpack -p"
}
}
프로덕션 환경에서 훅을 사용하여 프로덕션 빌드 스크립트를 자동으로 실행할 수 있습니다. 다음은 Heroku의 예시입니다.
{
"name": "my-project",
"scripts": {
"start": "webpack -d --watch",
"build": "webpack -p",
"heroku-postbuild": "webpack -p"
}
}
m
을 전역적으로 접근 가능하게 만들기
프로젝트 내 모든 곳에서 m
에 전역적으로 접근하려면 먼저 webpack.config.js
파일에서 다음과 같이 webpack
을 가져와야 합니다.
const webpack = require('webpack');
그런 다음 Webpack 설정 객체의 plugins
속성에 새 플러그인을 추가합니다.
{
plugins: [
new webpack.ProvidePlugin({
m: 'mithril',
}),
];
}
ProvidePlugin
에 대한 자세한 내용은 Webpack 문서를 참고하십시오.
React와의 차이점
Mithril의 JSX는 React의 JSX와 비교했을 때 미묘하지만 중요한 차이점들이 있습니다.
속성 및 스타일 속성 케이스 규칙
React는 data-*
및 aria-*
속성을 제외한 모든 속성에 대해 HTML 속성 이름 대신 카멜 케이스 DOM 속성 이름을 사용해야 합니다. 예를 들어 class
대신 className
을 사용하고 for
대신 htmlFor
를 사용합니다. Mithril에서는 소문자 HTML 속성 이름을 사용하는 것이 일반적입니다. Mithril은 해당 속성이 존재하지 않을 경우 속성 설정을 기본으로 사용하므로 HTML과 더욱 직관적으로 연결됩니다. 대부분의 경우 DOM 속성 및 HTML 속성 이름은 동일하거나 매우 유사합니다. 예를 들어 입력 요소의 value
/checked
속성과 HTML 요소의 elem.tabIndex
속성 대 tabindex
전역 속성이 있습니다. 케이스를 넘어 다른 경우는 거의 없습니다. class
속성에 대한 elem.className
속성 또는 for
속성에 대한 elem.htmlFor
속성은 몇 가지 예외 중 하나입니다.
마찬가지로 React는 항상 elem.style
속성을 통해 DOM에 노출된 카멜 케이스 스타일 속성 이름(예: cssHeight
및 backgroundColor
)을 사용합니다. Mithril은 둘 다 지원하며 케밥 케이스 CSS 속성 이름(예: height
및 background-color
)을 지원하며 일반적으로 후자를 선호합니다. cssHeight
, cssFloat
및 공급업체 접두사가 붙은 속성만 케이스 이상의 차이가 있습니다.
DOM 이벤트
React는 모든 이벤트 핸들러의 첫 글자를 대문자로 표기합니다. onClick
은 click
이벤트를 수신하고 onSubmit
은 submit
이벤트를 수신합니다. 일부는 여러 단어가 함께 연결되어 추가로 변경됩니다. 예를 들어 onMouseMove
는 mousemove
이벤트를 수신합니다. Mithril은 이러한 케이스 매핑을 사용하지 않고, 대신 기본 이벤트 이름 앞에 on
을 붙이는 방식을 사용합니다. 따라서 click
및 mousemove
이벤트를 수신하려면 onclick
및 onmousemove
리스너를 추가해야 합니다. 이는 HTML의 명명 규칙과 훨씬 더 밀접하게 일치하며 HTML 또는 일반 DOM 배경 지식이 있는 경우 훨씬 더 직관적입니다.
React는 캡처 단계(기본 버블 단계가 두 번째 패스에서 안쪽에서 바깥쪽으로 이동하는 것과 반대로 첫 번째 패스에서 바깥쪽에서 안쪽으로 이동)에서 이벤트 수신기를 등록하는 것을 지원합니다. Mithril은 현재 이러한 기능을 지원하지 않지만 향후 추가될 수 있습니다. 필요한 경우 라이프사이클 훅에서 수동으로 수신기를 추가하고 제거할 수 있습니다.
JSX vs hyperscript
JSX와 hyperscript는 vnode를 정의하는 데 사용되는 두 가지 다른 구문이며, 각각 장단점을 가지고 있습니다.
JSX는 HTML/XML에 익숙한 사용자에게 더욱 친숙하며, 해당 구문을 사용하여 DOM 요소를 정의하는 데 익숙한 사용자에게 적합합니다. 또한 구두점이 적고 속성에 시각적 노이즈가 적기 때문에 많은 경우 더 깔끔하게 보이며 가독성이 좋습니다. 대부분의 일반적인 편집기에서 HTML과 유사한 방식으로 DOM 요소에 대한 자동 완성 기능을 지원합니다. 하지만 사용하기 위해서는 추가적인 빌드 과정이 필요하며, 편집기 지원 범위가 일반 JavaScript만큼 넓지 않고, 코드 양이 상대적으로 많아지는 경향이 있습니다. 또한, 동적인 콘텐츠를 많이 다룰 때 모든 값에 대해 보간법을 사용해야 하므로 코드의 양이 다소 늘어날 수 있습니다.
Hyperscript는 HTML 또는 XML 사용 경험이 많지 않은 백엔드 JavaScript 개발자에게 더 친숙할 수 있습니다. 중복성이 적고 더 간결하며 정적 클래스, ID 및 기타 속성에 대한 CSS와 유사한 설탕을 제공합니다. 또한 빌드 단계 없이 사용할 수 있지만 원하는 경우 추가할 수 있습니다. 또한 "보간"할 필요가 없기 때문에 많은 동적 콘텐츠를 다룰 때 작업하기가 약간 더 쉬울 수 있습니다. 하지만 간결함 때문에 일부 사용자, 특히 프론트엔드 HTML/CSS/XML 경험이 적은 개발자에게는 가독성이 떨어질 수 있으며, ID, 클래스, 속성 등 hyperscript 선택자를 자동 완성해주는 플러그인은 아직까지 알려진 바가 없습니다.
더 복잡한 트리에서 장단점이 더욱 두드러집니다. 예를 들어 @dead-claudia의 실제 프로젝트에서 가져온 hyperscript 트리를 명확성과 가독성을 위해 약간 변경한 것을 고려해 보겠습니다.
function SummaryView() {
let tag, posts;
function init({ attrs }) {
Model.sendView(attrs.tag != null);
if (attrs.tag != null) {
tag = attrs.tag.toLowerCase();
posts = Model.getTag(tag);
} else {
tag = undefined;
posts = Model.posts;
}
}
function feed(type, href) {
return m('.feed', [
type,
m('a', { href }, m('img.feed-icon[src=./feed-icon-16.gif]')),
]);
}
return {
oninit: init,
// To ensure the tag gets properly diffed on route change.
onbeforeupdate: init,
view: () =>
m('.blog-summary', [
m('p', 'My ramblings about everything'),
m('.feeds', [
feed('Atom', 'blog.atom.xml'),
feed('RSS', 'blog.rss.xml'),
]),
tag != null
? m(TagHeader, { len: posts.length, tag })
: m('.summary-header', [
m('.summary-title', 'Posts, sorted by most recent.'),
m(TagSearch),
]),
m(
'.blog-list',
posts.map(post =>
m(
m.route.Link,
{
class: 'blog-entry',
href: `/posts/${post.url}`,
},
[
m(
'.post-date',
post.date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
),
m('.post-stub', [
m('.post-title', post.title),
m('.post-preview', post.preview, '...'),
]),
m(TagList, { post, tag }),
]
)
)
),
]),
};
}
다음은 JSX를 사용하여 위의 코드와 정확히 동일한 기능을 구현한 코드입니다. 이 코드를 통해 두 구문이 어떻게 다른지, 그리고 각각의 장단점이 무엇인지 확인할 수 있습니다.
function SummaryView() {
let tag, posts;
function init({ attrs }) {
Model.sendView(attrs.tag != null);
if (attrs.tag != null) {
tag = attrs.tag.toLowerCase();
posts = Model.getTag(tag);
} else {
tag = undefined;
posts = Model.posts;
}
}
function feed(type, href) {
return (
<div class="feed">
{type}
<a href={href}>
<img class="feed-icon" src="./feed-icon-16.gif" />
</a>
</div>
);
}
return {
oninit: init,
// To ensure the tag gets properly diffed on route change.
onbeforeupdate: init,
view: () => (
<div class="blog-summary">
<p>My ramblings about everything</p>
<div class="feeds">
{feed('Atom', 'blog.atom.xml')}
{feed('RSS', 'blog.rss.xml')}
</div>
{tag != null ? (
<TagHeader len={posts.length} tag={tag} />
) : (
<div class="summary-header">
<div class="summary-title">Posts, sorted by most recent</div>
<TagSearch />
</div>
)}
<div class="blog-list">
{posts.map(post => (
<m.route.Link class="blog-entry" href={`/posts/${post.url}`}>
<div class="post-date">
{post.date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</div>
<div class="post-stub">
<div class="post-title">{post.title}</div>
<div class="post-preview">{post.preview}...</div>
</div>
<TagList post={post} tag={tag} />
</m.route.Link>
))}
</div>
</div>
),
};
}
팁과 요령
HTML을 JSX로 변환
Mithril.js에서는 올바른 형식으로 작성된 HTML 코드가 일반적으로 유효한 JSX 코드로 인식됩니다. HTML 코드를 붙여넣는 것 외에 별도로 필요한 작업은 거의 없습니다. 일반적으로 attr=value
와 같이 따옴표로 묶이지 않은 속성 값은 attr="value"
형태로 변경하고, <input>
과 같은 빈(void) 요소는 <input />
와 같이 닫는 태그를 추가해야 합니다. 이는 JSX가 HTML이 아닌 XML 기반이기 때문입니다.
hyperscript를 사용할 때 HTML을 hyperscript 구문으로 변환해야 하는 경우가 많습니다. 이러한 변환 과정을 더욱 빠르게 처리하기 위해 커뮤니티에서 개발한 HTML-to-Mithril-template 변환기를 활용하여 대부분의 작업을 자동화할 수 있습니다.