이전 게시물에 이어서, 실제로 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 는 두가지 패키지를 필요로한다. autoprefixerpostcss.

$ 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