Skip to main content

Command Palette

Search for a command to run...

Ash Typescript

Updated
3 min readView as Markdown

TL;DR
Elixir의 Ash Framework로 정의한 리소스를 TypeScript와 바로 연결해 주는 툴.
백엔드 스펙을 따로 문서화하지 않아도 타입 안정성과 동기화를 자동으로 보장한다.


왜 사용하는가?

Elixir로 백엔드를, TypeScript로 프런트엔드를 작성하다 보면 변경되는 API 스펙을 항상 동기화된 상태로 관리하기 쉽지 않다.
결국 동기화를 위해서는 Graphql 을 사용하거나, 별도의 툴이 필요하다.
AshTypescript 는 Ash Resource 를 기반으로 TS type 을 자동으로 생성한다.
그리고 Type 만 제공하는데 그치는게 아니라 trpc 로 Ash Action 을 자동으로 제공한다.


주요 기능

  • 자동 타입 생성

  • RPC 함수 제공

  • 실시간 RPC (Phoenix 채널 기반)


예시 코드

설정

Igniter 를 사용하면 command 하나로 바로 설정이 완료된다.
react 를 사용하는 경우 2번째 command 사용.
react 를 명시하면, assets 의 의존성에 react 19 기반 세팅도 자동으로 된다.

# Add ash_typescript to your mix.exs and install
mix igniter.install ash_typescript

# For a full-stack Phoenix + React setup, use the --framework flag:
mix igniter.install ash_typescript --framework react

RPC 액션 정의

가장 기본적인 User Resource 를 trpc 로 노출하는 예제이다.
물론 User resource 의 policy 에서 trpc 로 호출해도 문제없게 세팅은 필요하다.

defmodule Hoin.Accounts do
  use Ash.Domain, otp_app: :hoin, extensions: [AshAdmin.Domain, AshTypescript.Rpc]

  admin do
    show? true
  end

  typescript_rpc do
    resource Hoin.Accounts.User do
      rpc_action :get_by_email, :get_by_email
      rpc_action :list_users, :read
      rpc_action :get_user, :read
    end
  end

  resources do
    resource Hoin.Accounts.Token
    resource Hoin.Accounts.User
  end
end

타입스크립트 코드 생성

이렇게만 하면 바로 FE 레벨에서 사용가능하다.

# Ash Codegen
mix ash.codegen --dev

# Ash Typescript 직접 사용
mix ash_typescript.codegen --output "assets/js/ash_rpc.ts"

FE 사용 예시

아래 코드는 전부 typesafe 하다.

import { getByEmail, listUsers, buildCSRFHeaders } from "./ash_rpc";

const fetchUserByEmail = async (email: string) => {
  const result = await getByEmail({ 
    input: { email: email }, 
    fields: ["email", "id"],
    headers: buildCSRFHeaders()
  });

  if (result.success) {
    return result.data;
  } else {
    throw new Error(result.errors?.[0]?.message || 'Failed to fetch user');
  }
};

const fetchAllUsers = async () => {
  const result = await listUsers({
    fields: ["id", "email"],
    headers: buildCSRFHeaders()
  });

  if (result.success) {
    return result.data;
  } else {
    throw new Error(result.errors?.[0]?.message || 'Failed to fetch users');
  }
};

Igniter 사용 시 기본적으로 tsconfig.json 이 최소한의 세팅만 되어있다.
위 코드예시처럼 promise / await 을 사용하거나,
result.success 로 if 분기를 했을 때 타입 safe 하게 보장받으려면 몇가지 추가할 항목이 있다.

  • target: 나는 ES2015 를 사용한다. Promise / Await 구조를 제공하는 가장 오래된 spec 이다.

  • strict: true 로 설정하지 않으면, success 값에 따라 달라지는 타입 구조에 대한 참조에서 에러가 발생한다.

  • moduleResolution: import 구문을 node 스타일로 맞추기 위해 “node” 로 설정한다.


마무리

LiveView 를 사용하는 큰 이유중 하나였던 타입 동기화 문제를 깔끔하게 해결해주는 솔루션이다.
마침 Tidewave 에서 React 를 지원하는데, 기존 프로젝트를 React 기반으로 변경하면 풀스택으로 LLM 의 도움을 간단하게 받을 수 있어서 꽤나 도움이 될 것 같다는 생각이 든다.

More from this blog

Ash Reactor

TLDR; Reactor는 복잡한 워크플로우를 단계별로 정의하고 실행하는 라이브러리다. 각 단계가 독립적으로 돌아가며, 병렬 처리와 에러 핸들링도 쉽게 할 수 있다. 왜 쓸까? 프로젝트에서 "데이터 조회 → 가공 → 외부 API 호출 → 저장" 같은 복잡한 로직을 처리할 때가 있다.이런걸 그냥 함수로 쭉 연결하면 코드가 복잡해지고 에러 처리도 어려워진다. Reactor를 쓰면 각 단계를 명확히 분리하고 선언적으로 워크플로우를 관리할 수 있다. ...

Sep 21, 20254 min read

Ash StateMachine

TLDR; Ash StateMachine 은 상태 변경이 가능한 케이스를 명시하고, 미리 선언된 상태 변경만 가능하도록 한다. 왜 쓸까? 예를 들면, “새로 생성 → 진행 중 → 완료” 처럼 정해진 workflow 가 있다면, 로직으로 if/else 를 사용하는 것보다 상태머신으로 선언해 두는 게 안전하고 읽기 쉽다. 예제: Task 요구사항: 기본 상태는 :new :start 액션이 :new → :in_progress :finish ...

Sep 14, 20252 min read

Ash Oban

최근 작업중에 이메일 발송을 처리해야 했다.after_action 에 이마일 발송을 고려했는데 최종적으로는 AshOban 을 사용했다. AshOban이 뭔가? Ash Framework와 Oban을 이어주는 라이브러리다. 백그라운드 job을 Ash 스타일로 쓸 수 있게 해준다. 멤버 초대 이메일 보내기 클럽에 새 멤버를 초대하면 이메일을 보내야 하는 상황이다. 이걸 어떻게 처리할까? Action Hook으로 하면: 사용자가 서버에서 이메일 발송...

Aug 31, 20252 min read

kkbz.hop

14 posts