JSX
hono/jsx를 사용하면 JSX 문법으로 HTML을 작성할 수 있다.
hono/jsx는 클라이언트 측에서도 동작하지만, 주로 서버 측에서 콘텐츠를 렌더링할 때 사용하게 될 것이다. 서버와 클라이언트 모두에 공통적으로 적용되는 JSX 관련 내용을 살펴보자.
설정
JSX를 사용하려면 tsconfig.json을 다음과 같이 수정한다:
tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}또는 프라그마 지시자를 사용할 수도 있다:
/** @jsx jsx */
/** @jsxImportSource hono/jsx */Deno를 사용하는 경우, tsconfig.json 대신 deno.json을 수정해야 한다:
{
"compilerOptions": {
"jsx": "precompile",
"jsxImportSource": "hono/jsx"
}
}사용법
index.tsx:
import { Hono } from 'hono'
import type { FC } from 'hono/jsx'
const app = new Hono()
const Layout: FC = (props) => {
return (
<html>
<body>{props.children}</body>
</html>
)
}
const Top: FC<{ messages: string[] }> = (props: {
messages: string[]
}) => {
return (
<Layout>
<h1>Hello Hono!</h1>
<ul>
{props.messages.map((message) => {
return <li>{message}!!</li>
})}
</ul>
</Layout>
)
}
app.get('/', (c) => {
const messages = ['Good Morning', 'Good Evening', 'Good Night']
return c.html(<Top messages={messages} />)
})
export default app프래그먼트
여러 엘리먼트를 그룹화할 때 추가 노드를 생성하지 않으려면 프래그먼트를 사용한다:
import { Fragment } from 'hono/jsx'
const List = () => (
<Fragment>
<p>첫 번째 자식</p>
<p>두 번째 자식</p>
<p>세 번째 자식</p>
</Fragment>
)또는 설정이 되어 있다면 <></>를 사용해 작성할 수도 있다.
const List = () => (
<>
<p>첫 번째 자식</p>
<p>두 번째 자식</p>
<p>세 번째 자식</p>
</>
)PropsWithChildren
PropsWithChildren을 사용하면 함수형 컴포넌트에서 자식 엘리먼트를 올바르게 추론할 수 있다.
import { PropsWithChildren } from 'hono/jsx'
type Post = {
id: number
title: string
}
function Component({ title, children }: PropsWithChildren<Post>) {
return (
<div>
<h1>{title}</h1>
{children}
</div>
)
}Raw HTML 직접 삽입
HTML을 직접 삽입하려면 dangerouslySetInnerHTML을 사용한다:
app.get('/foo', (c) => {
const inner = { __html: 'JSX · SSR' }
const Div = <div dangerouslySetInnerHTML={inner} />
})메모이제이션
memo를 사용해 계산된 문자열을 메모이제이션하여 컴포넌트를 최적화한다:
import { memo } from 'hono/jsx'
const Header = memo(() => <header>Welcome to Hono</header>)
const Footer = memo(() => <footer>Powered by Hono</footer>)
const Layout = (
<div>
<Header />
<p>Hono is cool!</p>
<Footer />
</div>
)Context
useContext를 사용하면 props를 통해 값을 전달하지 않고도 컴포넌트 트리의 어느 레벨에서나 데이터를 전역적으로 공유할 수 있다.
import type { FC } from 'hono/jsx'
import { createContext, useContext } from 'hono/jsx'
const themes = {
light: {
color: '#000000',
background: '#eeeeee',
},
dark: {
color: '#ffffff',
background: '#222222',
},
}
const ThemeContext = createContext(themes.light)
const Button: FC = () => {
const theme = useContext(ThemeContext)
return <button style={theme}>Push!</button>
}
const Toolbar: FC = () => {
return (
<div>
<Button />
</div>
)
}
// ...
app.get('/', (c) => {
return c.html(
<div>
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
</div>
)
})비동기 컴포넌트
hono/jsx는 비동기 컴포넌트를 지원한다. 따라서 컴포넌트 내에서 async/await를 사용할 수 있다. c.html()로 렌더링하면 자동으로 await 처리가 된다.
const AsyncComponent = async () => {
await new Promise((r) => setTimeout(r, 1000)) // 1초 대기
return <div>Done!</div>
}
app.get('/', (c) => {
return c.html(
<html>
<body>
<AsyncComponent />
</body>
</html>
)
})Suspense Experimental
React와 유사한 Suspense 기능을 사용할 수 있다. 비동기 컴포넌트를 Suspense로 감싸면, fallback에 있는 내용이 먼저 렌더링되고, Promise가 해결되면 기다렸던 내용이 표시된다. renderToReadableStream()과 함께 사용할 수 있다.
import { renderToReadableStream, Suspense } from 'hono/jsx/streaming'
//...
app.get('/', (c) => {
const stream = renderToReadableStream(
<html>
<body>
<Suspense fallback={<div>loading...</div>}>
<Component />
</Suspense>
</body>
</html>
)
return c.body(stream, {
headers: {
'Content-Type': 'text/html; charset=UTF-8',
'Transfer-Encoding': 'chunked',
},
})
})ErrorBoundary Experimental
ErrorBoundary를 사용하면 자식 컴포넌트에서 발생한 오류를 잡을 수 있다.
아래 예제에서는 오류가 발생할 경우 fallback에 지정한 내용을 보여준다.
function SyncComponent() {
throw new Error('Error')
return <div>Hello</div>
}
app.get('/sync', async (c) => {
return c.html(
<html>
<body>
<ErrorBoundary fallback={<div>Out of Service</div>}>
<SyncComponent />
</ErrorBoundary>
</body>
</html>
)
})ErrorBoundary는 비동기 컴포넌트와 Suspense와 함께 사용할 수도 있다.
async function AsyncComponent() {
await new Promise((resolve) => setTimeout(resolve, 2000))
throw new Error('Error')
return <div>Hello</div>
}
app.get('/with-suspense', async (c) => {
return c.html(
<html>
<body>
<ErrorBoundary fallback={<div>Out of Service</div>}>
<Suspense fallback={<div>Loading...</div>}>
<AsyncComponent />
</Suspense>
</ErrorBoundary>
</body>
</html>
)
})HTML 미들웨어와의 통합
JSX와 HTML 미들웨어를 결합해 강력한 템플릿 기능을 활용할 수 있다. 더 자세한 내용은 HTML 미들웨어 문서를 참고한다.
import { Hono } from 'hono'
import { html } from 'hono/html'
const app = new Hono()
interface SiteData {
title: string
children?: any
}
const Layout = (props: SiteData) =>
html`<!doctype html>
<html>
<head>
<title>${props.title}</title>
</head>
<body>
${props.children}
</body>
</html>`
const Content = (props: { siteData: SiteData; name: string }) => (
<Layout {...props.siteData}>
<h1>Hello {props.name}</h1>
</Layout>
)
app.get('/:name', (c) => {
const { name } = c.req.param()
const props = {
name: name,
siteData: {
title: 'JSX with html sample',
},
}
return c.html(<Content {...props} />)
})
export default appJSX 렌더러 미들웨어 활용
JSX 렌더러 미들웨어를 사용하면 JSX를 통해 HTML 페이지를 더 쉽게 만들 수 있다.
타입 정의 재정의
여러분은 커스텀 엘리먼트와 속성을 추가하기 위해 타입 정의를 재정의할 수 있다.
declare module 'hono/jsx' {
namespace JSX {
interface IntrinsicElements {
'my-custom-element': HTMLAttributes & {
'x-event'?: 'click' | 'scroll'
}
}
}
}