Web studio part 2

Part 1에 이어 project 생성, 컴포넌트 구성 과정을 분석해 봅니다.

2023-09-10

Tech Stack

tRPC

TypeScript로 full stack을 개발하면 server와 client 사이의 요청의 파라미터와 응답의 형식을 TypeScript로 관리하고 싶어집니다.

빠른 개발 또는 잦은 요구사항 변경이 필요한 환경에서는 API 사양 변경역시 자주 발생합니다. 이때 TypeScript의 도움을 받을 수 있다면, API 파라미터나 응답의 이름 변경, 삭제, 추가에 빠르고 정확하게 대응할 수 있습니다.

REST, GraphQL의 경우 이러한 이점을 활용하려면 별도의 type 생성기가 필요합니다. Type 생성기는 코드 생성을 수반하기 때문에 매끄러운 개발 환경 구성을 위한 노력이 필요합니다.

반면, tRPC는 server API 라우트의 형식을 typeof 키워드로 가져와 client를 생성하면 type checking 환경을 바로 구현할 수 있습니다. 따라서 server 코드의 작성과 동시에 client 형검사가 가능합니다.

nanostores/nanostores

프레임워크에 종속적이지 않은 store 구현체 입니다.

atom을 사용하여 immutable 상태를 공유하고 reactive한 처리가 가능합니다.

action, task 등으로 store의 비동기 초기화 등이 가능합니다.

Micro front end 구성 시 서로 다른 프레임워크간 상태 공유가 가능한 이점이 있습니다.

webstudio-is/immerhin

immer 패키지의 Patches 기능을 활용하여 client, server 간 객체 상태 공유를 위한 라이브러리 입니다.

Web studio에서 사용하기 위해 자체 개발한 라이브러리 이며, Web studio와 관계없이 독립적인 사용이 가능합니다.

프로젝트 생성

Remix는 별도의 API routing 방식을 사용하지 않습니다 (API Routes | Remix). 가이드에 따르면, routes 폴더에 있는 파일을 기준으로 UI 렌더링을 위한 코드와 API 요청 처리 코드를 대략 다음과 같이 작성합니다.

  • 렌더링을 위한 UI 컴포넌트: default export
  • GET 요청 처리: export async function loader
  • GET 요청을 제외한 API 처리: export async function action

이러한 Remix 만의 API 호출 방식을 따르면서 tRPC 호출 처리를 하기 위해 다소 복잡한 구조를 가지고 있습니다.

공식 가이드에 따르면 tRPC client는 createTRPCProxyClient 함수를 사용하여 호출하도록 되어 있습니다. 하지만 Web studio는 별도의 tRPC 서버를 구성하지 않고 tRPC 요청을 Remix 프레임워크에서 받아내기 위해 xxx.xxx.$method.ts 형식의 라우트를 구성합니다. 그리고 이 라우트에서 tRPC 서버단 코드를 수행합니다.

Web studio client 에서는 Remix의 action 구현체 호출을 위해 useFetcher를 호출할 수 있는 nested proxy 객체를 생성합니다. tRPC의 서버 route 정의로 부터 mutation을 수행하는 경우 useMutation을 그렇지 않은 경우 useQuery 호출이 가능한 proxy 객체를 만들어 냅니다. 이 proxy 객체는 route의 recode를 첫번째 key 값으로 가지고 useMutation을 호출할 경우 POSTuseQuery 를 호출할 경우 GET 요청으로 치환하여 useFetchersubmit을 호출 합니다.

이 과정을 도식화하면 다음과 같습니다.

Alt text

첫 번째 /dashboard 요청은 loader 함수를 통해 필요한 데이터를 server 단에서 취합합니다. tRPC로 정의한 findMany, findManyByIds와 같은 route를 직접 호출하며 각 route는 Prisma를 사용하여 데이터베이스를 호출하고 응답을 처리합니다.

사용자가 프로젝트 생성 다이얼로그의 버튼을 누르면 Remix의 useFetchersubmit을 호출합니다. Remix route 프레임워크에 의해 dashboard.project.$method.tsx 파일의 loader 함수를 호출하고 이는 다시 서버단의 tRPC 구현체를 호출합니다.

다음은 project의 모델 입니다. 생성할 때 필요한 인자는 title 입니다.

view DashboardProject {
  id          String   @id @default(uuid())
  createdAt   DateTime @default(now())
  title       String
  domain      String
  userId      String?
  isDeleted   Boolean  @default(false)
  isPublished Boolean
}

빌더(builder)

Alt text

프로젝트 생성 후, 프로젝트 카드를 클릭하면 builder 화면을 표시합니다.

CSS grid로 레이아웃을 잡았습니다. 가운데 main 영역은 <iframe/>을 배치하였으며, side bar를 drag하여 resizing이 가능합니다. resizing은 pointerup, pointerdown, pointermove 이벤트를 처리합니다. (코드) resizing은 canvasWidthStore 값을 변경합니다. canvasWidthStore값은 useCanvasStyle hook을 사용하여 width를 변경합니다.

왼쪽 navigation 영역에서 컴포넌트 추가를 할 수 있습니다.

Alt text

컴포넌트 팔레트 중 하나를 드래그 하면, instancesStore의 값에 관련 정보를 추가 합니다.

instanceStore는 immerhint가 관리하는 store이며 해당 값은 서버 동기화를 위해 /rest/patch 요청으로 전달합니다.

이렇게 immerhint에 의해 관리하는 초기화 과정 중에 설정합니다. (코드)

rest.patch.ts에서 이 요청을 받으면, 다음과 같은 Build 모델의 형식으로 Prisma를 사용해 저장하게 됩니다.

model Build {
  id        String   @unique @default(uuid())
  version   Int      @default(0)
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt
  pages     String

  project   Project @relation(fields: [projectId], references: [id])
  projectId String

  breakpoints           String @default("[]")
  styles                String @default("[]")
  styleSources          String @default("[]")
  styleSourceSelections String @default("[]")
  props                 String @default("[]")
  dataSources           String @default("[]")
  instances             String @default("[]")

  deployment    String?
  publishStatus PublishStatus @default(PENDING)

  @@id([id, projectId])
}

컴포넌트 팔레트에서 drag & drop을 사용하여 컴포넌트를 배치하는 경우 instances의 정보를 업데이트 하게 됩니다.

마치며

프로젝트 생성과 컴포넌트 추가 과정을 살펴 보았습니다. Remix와 tRPC의 장점을 얻기위한 구조가 인상 깊었습니다. 물론 이로인해 구현이 다소 복잡해졌지만 tRPC의 route 추가와 파라미터, 응답의 변화에 기민하게 대응하면서 remix의 구현 의도를 해치지 않도록 고민을 한 흔적이 보였습니다.

또한 지속적으로 변하는 drawing 정보를 데이터 베이스에 동기화 하기 위해, immerhint를 사용한 부분은 놀라웠습니다. 비교적 최신 프로젝트 답게 nanostore를 single source of truth로 사용하고 immerhint를 통해 서버와 동기화하는 부분에서 많은 아이디어를 얻었습니다.

다만 단일 사용자 수정이 가능한 one way 동기화 방식임으로 여러명이 협업하고 동시 편집하는 기능 구현은 시간을 두고 지켜봐야 할 것 같습니다.

Loading script...