Phoenix LiveView + Typescript + Svelte + Tailwind CSS, How.
이전 게시물에 이어서, 실제로 Phoenix Liveview 에 Svelte 를 올려보겠다.
이 튜토리얼에서는 DB 연결이 필요하지 않기 때문에 ecto 를 제외하고 liveview 만 활성화한 형태로 프로젝트를 생성한다.
$ mix phx.new phovelte --live --no-ecto
Typescript
우선 typescript 를 먼저 세팅해본다.
$ cd assets
$ yarn add -D typescript ts-loader && npx tsc --init
Phoenix LiveView 에 기본적으로 탑제(?)되어 있는 package 에 대한 type definition 을 설치해준다.
$ yarn add -D @types/phoenix @types/nprogress @types/phoenix_live_view
app.js 를 app.ts 로 변경하면, type 오류가 나는데, 크게 중요한 부분은 아니기 때문에 간다하게 아래처럼 처리해준다.
...
let csrfToken = document.querySelector("meta[name='csrf-token']")?.getAttribute("content")
...
(window as any).liveSocket = liveSocket
ts 파일을 인식할 수 있도록 webpack.config.js 를 수정한다.
entry: {
'app': glob.sync('./vendor/**/*.ts').concat(['./js/app.ts'])
},
...
rules: [
{
test: /\.(js|ts)$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader'
}, {
loader: 'ts-loader',
}]
},
...
]
root 로 돌아가서 실행해보자.
$ cd .. && mix phx.server
정상적으로 phoenix 서버가 typescript 와 구동되는 것을 확인한다. 간혹오타나 기타 문제 때문에 제대로 화면이 그려지지 않을때에는 browser console 을 확인해보자.
Svelte
이제 svelte 를 설정할 차례이다.
$ yarn add -D svelte svelte-loader svelte-preprocess @tsconfig/svelte @types/webpack-env
필요한 패키지들을설치하고, webpack 에서 svelte 를 컴파일 할 수 있도록 수정해준다. .mjs
는 경우 webpack 에서 require 함수를 사용하기 위한 설정이다.
entry: {
...
},
resolve: {
alias: {
svelte: path.resolve('node_modules', 'svelte')
},
extensions: ['.mjs', '.js', '.ts', '.svelte'],
mainFields: ['svelte', 'browser', 'module', 'main'],
modules: ['node_modules'],
},
...
rules: [
{
test: /\.(js|ts)$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader'
}, {
loader: 'ts-loader',
}]
},
{
test: /\.mjs$/,
include: /node_modules/,
type: "javascript/auto",
},
{
test: /\.(html|svelte)$/,
exclude: /node_modules/,
use: {
loader: 'svelte-loader',
options: {
hotReload: true,
preprocess: require('svelte-preprocess')({})
}
}
},
...
]
tsconfig.json 을 수정한다.
{
"extends": "@tsconfig/svelte/tsconfig.json",
"include": [
"js/**/*"
],
"exclude": [
"node_modules/*",
"public/*"
],
"compilerOptions": {
"isolatedModules": false,
"types": [
"node",
"webpack-env"
],
"typeRoots": [
"@types"
],
}
}
hook 을 통해 svelte component를 불러온다.
let liveSocket = new LiveSocket("/live", Socket, {
hooks: {
'svelte-component': {
mounted(this: {el: HTMLElement}) {
const componentName = this.el.getAttribute('data-name');
if (!componentName) {
throw new Error('Component name must be provided');
}
const requiredApp = require(`./${componentName}.svelte`);
if (!requiredApp) {
throw new Error(`Unable to find ${componentName} component`);
}
const props = this.el.getAttribute('data-props');
const parsedProps = props ? JSON.parse(props) : {};
new requiredApp.default({
target: this.el,
props: parsedProps,
});
},
},
},
params: {
_csrf_token: csrfToken,
},
});
테스트를 위해 템플릿 컴포넌트를 하나 작성해보자. assets/js/Template.svelte
<script lang="ts">
export let name: string;
</script>
<h1 class="text-xl">
Phoenix and {name}!
</h1>
일단 기본 설정은 되었고, 이제 phoenix 에서 그려줄 차례.
먼저 json 파싱을 위해 Poison 을 설치한다. 기본으로 설치되는 Jason 을 사용해도 무방하지만, elixir 의 struct 를 recusrive 하게 파싱하지 못하기 때문에 Poison 을 추천.
mix.ex
...
defp deps do
[
...,
{:poison, "~> 3.1"}
]
...
Svelte component 는 liveview component 와 hook 을 이용해 인스턴스로 만들 계획이다. 그걸 위해 phoenix live_component 인 SvelteComponent 를 만든다.
lib/phovelte_web/components/svelte_component.ex
defmodule PhovelteWeb.SvelteComponent do
use PhovelteWeb, :live_component
alias AssetsTracker.Utils.Randomizer
def render(assigns) do
~L"""
<span id="<%= generate_id(@name) %>" data-name="<%= @name %>" data-props="<%= json(@props) %>" phx-update="ignore" phx-hook="svelte-component"></span>
"""
end
defp json(props) do
props
|> Poison.encode
|> case do
{:ok, message} ->
message
{:error, reason} ->
IO.inspect(reason)
""
end
end
defp generate_id(name) do
"svelte-#{String.replace(name, " ", "-")}-#{get_random_numbers()}"
end
defp get_random_numbers do
Enum.random(0..1000000000000)
end
end
id 를 uuid 등으로 만들면 좋다. ex) Ecto.UUID.generate 다만 이 튜토리얼의 경우 ecto 를 설치하지도 않았기 때문에 간단하게 random number 로 처리하겠다.
page_live.html.leex 에 아래와 같이 PhovelteWeb.SvelteComponent
를 추가한다.
<%= live_component @socket, PhovelteWeb.SvelteComponent, name: "Template", props: %{name: "Phovelte!"} %>
<section class="phx-hero">
...
짠
Tailwind CSS
부록으로 Tailwind CSS 를 설정해보겠다.
tailwind 는 두가지 패키지를 필요로한다. autoprefixer
와 postcss
.
$ cd assets && yarn add -D autoprefixer tailwindcss postcss postcss-loader
필요한 패키지를 설치했으면 tailwind 를 초기화해준다.
$ npx tailwindcss init
postcss 가 돌아가도록 postcss.config.js 만들어준ㅏ.
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
webpack.config.js 에 postcss-loader 를 추가한다.
rules: [
...
{
test: /\.[s]?css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
'postcss-loader',
],
}
]
app.scss 에서 tailwind 를 로드해준다.
/* This file is for your main application css. */
@import "./phoenix.css";
@import "../node_modules/nprogress/nprogress.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
/* LiveView specific classes for your customizations */
.phx-no-feedback.invalid-feedback,
...
}
Template.svelte 파일을 수정해보자.
...
<h1 class="text-xl bg-blue-500 font-bold">
Phoenix and {name}!
</h1>
이렇게 못생겨지면 성공
간혹 적용이 안될때가 있는데 캐시 문제로 보인다. app.scss 파일을 수정하고 저장하면 잘 된다.
여기까지가 Phoenix LiveView 에 Typescript 와 Svelte, TailwindCSS 를 설정해보았다. 다만, eex 나 leex 파일에서 <script>
태그로 코딩된 부분에는 적용되지 않는다. app.ts
에서 hook 이나 다른 방법으로 transpile 된 코드를 src
로 적용하는 방법을 고려하면 될 듯.
아래 소스코드에는 svelte instance 를 destroy 하는 코드까지 포함했으나 svelte 버젼에 따라 사용 방식이 달라질 수 있다. (private method 를 사용하였음)
Github 소스코드 https://github.com/colus001/phoenix-liveview-svelte-tailwind