게시일: 2025년 1월 31일
브라우저에서 프런트엔드뿐만 아니라 백엔드까지 모든 기능이 작동하는 블로그를 실행한다고 상상해 보세요. 서버나 클라우드가 필요하지 않습니다. 사용자, 브라우저, WebAssembly만 있으면 됩니다. 서버 측 프레임워크를 로컬에서 실행할 수 있도록 허용함으로써 WebAssembly는 기존 웹 개발의 경계를 허물고 흥미로운 새로운 가능성을 열어주고 있습니다. 이 게시물에서는 Evil Martians의 백엔드 책임자인 블라디미르 데멘티예프가 Ruby on Rails를 Wasm 및 브라우저 지원으로 만드는 작업의 진행 상황을 공유합니다.
- 15분 만에 Rails를 브라우저로 가져오는 방법
- Rails wasmification의 비하인드 스토리
- Rails와 Wasm의 미래
Ruby on Rails의 유명한 '15분 만에 블로그 만들기'가 이제 브라우저에서 바로 실행됩니다.
Ruby on Rails는 개발자 생산성과 빠른 출시에 중점을 둔 웹 프레임워크입니다. GitHub 및 Shopify와 같은 업계 선도 기업에서 사용하는 기술입니다. 이 프레임워크의 인기는 수년 전 데이비드 하인리메이어 한손 (DHH)이 게시한 유명한 '15분 만에 블로그를 만드는 방법' 동영상이 출시되면서 시작되었습니다. 2005년에는 이렇게 짧은 시간에 완전히 작동하는 웹 애플리케이션을 빌드하는 것이 상상도 할 수 없었습니다. 마법을 부린 것 같았습니다.
오늘은 브라우저에서 완전히 실행되는 Rails 애플리케이션을 만들어 이 마법 같은 느낌을 다시 살려 보겠습니다. 먼저 일반적인 방식으로 기본 Rails 애플리케이션을 만든 다음 Wasm용으로 패키징합니다.
배경: 명령줄에서 '15분 만에 블로그 만들기'
컴퓨터에 Ruby 및 Ruby on Rails가 설치되어 있다고 가정하면 먼저 새 Ruby on Rails 애플리케이션을 만들고 일부 기능을 스캐폴딩합니다 (원래 '15분 안에 블로그 만들기' 동영상과 동일).
$ rails new --css=tailwind web_dev_blog
create .ruby-version
...
$ cd web_dev_blog
$ bin/rails generate scaffold Post title:string date:date body:text
create db/migrate/20241217183624_create_posts.rb
create app/models/post.rb
...
$ bin/rails db:migrate
== 20241217183624 CreatePosts: migrating ====================
-- create_table(:posts)
-> 0.0017s
== 20241217183624 CreatePosts: migrated (0.0018s) ===========
이제 코드베이스를 건드리지 않고도 애플리케이션을 실행하고 작동하는 모습을 확인할 수 있습니다.
$ bin/dev
=> Booting Puma
=> Rails 8.0.1 application starting in development
...
* Listening on http://127.0.0.1:3000
이제 http://localhost:3000/posts에서 내 블로그를 열고 게시물을 작성할 수 있습니다.
몇 분 만에 매우 간단하지만 작동하는 블로그 애플리케이션을 빌드했습니다. 풀 스택 서버 제어형 애플리케이션입니다. 데이터를 유지하는 데이터베이스(SQLite), HTTP 요청을 처리하는 웹 서버 (Puma), 비즈니스 로직을 유지하고 UI를 제공하며 사용자 상호작용을 처리하는 Ruby 프로그램이 있습니다. 마지막으로 브라우징 환경을 간소화하는 얇은 JavaScript 레이어 (Turbo)가 있습니다.
공식 Rails 데모는 이 애플리케이션을 베어 메탈 서버에 배포하여 프로덕션 준비 상태로 만드는 방향으로 계속 진행됩니다. 여정은 반대 방향으로 계속됩니다. 애플리케이션을 먼 곳에 배치하는 대신 로컬에 '배포'합니다.
다음 단계: Wasm에서 '15분 만에 블로그 만들기'
WebAssembly가 추가된 이후 브라우저는 JavaScript 코드뿐만 아니라 Wasm으로 컴파일할 수 있는 모든 코드를 실행할 수 있게 되었습니다. Ruby도 예외가 아닙니다. 물론 Rails는 Ruby 이상이지만 차이점을 자세히 살펴보기 전에 데모를 계속 진행하고 Rails 애플리케이션을 wasmify (wasmify-rails 라이브러리에서 만든 동사)해 보겠습니다.
블로그 애플리케이션을 Wasm 모듈로 컴파일하고 브라우저에서 실행하려면 몇 가지 명령어만 실행하면 됩니다.
먼저 번들러 (Ruby의 npm
)를 사용하여 wasmify-rails 라이브러리를 설치하고 Rails CLI를 사용하여 생성기를 실행합니다.
$ bundle add wasmify-rails
$ bin/rails wasmify:install
create config/wasmify.yml
create config/environments/wasm.rb
...
info ✅ The application is prepared for Wasm-ificaiton!
wasmify:rails
명령어는 기본 '개발', '테스트', '프로덕션' 환경 외에도 전용 'wasm' 실행 환경을 구성하고 필요한 종속 항목을 설치합니다. 그린필드 Rails 애플리케이션의 경우 이렇게 하면 Wasm 지원을 추가하기에 충분합니다.
그런 다음 Ruby 런타임, 표준 라이브러리, 모든 애플리케이션 종속 항목이 포함된 핵심 Wasm 모듈을 빌드합니다.
$ bin/rails wasmify:build
==> RubyWasm::BuildSource(3.3) -- Building
...
==> RubyWasm::CrossRubyProduct(ruby-3.3-wasm32-unknown-wasip1-full-4aaed4fbda7afe0bdf4e22167afd101e) -- done in 47.37s
INFO: Packaging gem: rake-13.2.1
...
INFO: Packaging gem: wasmify-rails-0.2.0
INFO: Packaging setup.rb: bundle/setup.rb
INFO: Size: 73.77 MB
이 단계는 다소 시간이 걸릴 수 있습니다. 서드 파티 라이브러리의 네이티브 확장 프로그램 (C로 작성됨)을 올바르게 연결하려면 소스에서 Ruby를 빌드해야 합니다. 이 (일시적인) 단점은 게시물 뒷부분에서 다룹니다.
컴파일된 Wasm 모듈은 애플리케이션의 기초에 불과합니다. 애플리케이션 코드 자체와 모든 애셋 (예: 이미지, CSS, JavaScript)도 패키징해야 합니다. 패킹하기 전에 브라우저에서 wasmified Rails를 실행하는 데 사용할 수 있는 기본 런처 애플리케이션을 만듭니다. 이를 위해 발전기 명령어도 있습니다.
$ bin/rails wasmify:pwa
create pwa
create pwa/boot.html
create pwa/boot.js
...
prepend config/wasmify.yml
이전 명령어는 Vite로 빌드된 최소 PWA 애플리케이션을 생성합니다. 이 애플리케이션은 로컬에서 컴파일된 Rails Wasm 모듈을 테스트하는 데 사용하거나 정적 방식으로 배포하여 앱을 배포할 수 있습니다.
이제 런처를 사용하면 전체 애플리케이션을 단일 Wasm 바이너리로 패킹하기만 하면 됩니다.
$ bin/rails wasmify:pack
...
Packed the application to pwa/app.wasm
Size: 76.2 MB
작업이 끝났습니다. 실행기 앱을 실행하고 브라우저 내에서 완전히 실행되는 Rails 블로그 애플리케이션을 확인합니다.
$ cd pwa/
$ yarn dev
VITE v4.5.5 ready in 290 ms
➜ Local: http://localhost:5173/
http://localhost:5173으로 이동하여 '실행' 버튼이 활성화될 때까지 잠시 기다린 후 클릭합니다. 브라우저에서 로컬로 실행되는 Rails 앱을 사용해 보세요.
머신뿐만 아니라 브라우저 샌드박스 내에서 모놀리식 서버 측 애플리케이션을 실행하는 것이 마법처럼 느껴지지 않나요? 저는 '마법사'이지만 여전히 판타지처럼 보입니다. 하지만 마술이 아니라 기술의 발전이 있었습니다.
데모)에서는
도움말에 삽입된 데모를 사용해 보거나 독립형 창에서 데모를 실행할 수 있습니다. GitHub의 소스 코드를 확인하세요.
Wasm의 Rails 비하인드 스토리
서버 측 애플리케이션을 Wasm 모듈로 패킹하는 문제와 해결 방법을 더 잘 이해할 수 있도록 이 게시물의 나머지 부분에서는 이 아키텍처의 일부인 구성요소를 설명합니다.
웹 애플리케이션은 애플리케이션 코드를 작성하는 데 사용되는 프로그래밍 언어 외에도 더 많은 항목에 종속됩니다. 또한 각 구성요소는 _로컬 배포 환경_인 브라우저로 가져와야 합니다. '15분 만에 블로그 만들기' 데모의 흥미로운 점은 애플리케이션 코드를 다시 작성하지 않고도 이 작업을 실행할 수 있다는 것입니다. 기존 서버 측 모드와 브라우저에서 애플리케이션을 실행하는 데 동일한 코드가 사용되었습니다.
Ruby on Rails와 같은 프레임워크는 인프라 구성요소와 통신하는 추상화인 인터페이스를 제공합니다. 다음 섹션에서는 프레임워크 아키텍처를 사용하여 다소 난해한 로컬 제공 요구사항을 처리하는 방법을 설명합니다.
기반: ruby.wasm
Ruby는 2022년에 공식적으로 Wasm 지원(버전 3.2.0부터)을 시작했습니다. 즉, C 소스 코드를 Wasm으로 컴파일하고 원하는 위치로 Ruby VM을 가져올 수 있습니다. ruby.wasm 프로젝트는 브라우저 (또는 다른 JavaScript 런타임)에서 Ruby를 실행하기 위해 사전 컴파일된 모듈과 JavaScript 바인딩을 제공합니다. ruby:wasm 프로젝트에는 추가 종속 항목이 있는 맞춤 Ruby 버전을 빌드할 수 있는 빌드 도구도 함께 제공됩니다. 이는 C 확장자가 있는 라이브러리를 사용하는 프로젝트에 매우 중요합니다. 예, 네이티브 확장 프로그램을 Wasm으로 컴파일할 수도 있습니다. 아직 확장 프로그램은 아니지만 대부분의 확장 프로그램이 지원됩니다.
현재 Ruby는 WebAssembly 시스템 인터페이스인 WASI 0.1을 완전히 지원합니다. 구성요소 모델이 포함된 WASI 0.2는 이미 알파 상태이며 완료까지 몇 단계만 남았습니다. WASI 0.2가 지원되면 새 네이티브 종속 항목을 추가할 때마다 전체 언어를 다시 컴파일해야 하는 현재의 필요성이 사라집니다. 구성요소화할 수 있습니다.
부수적인 효과로 구성요소 모델은 번들 크기를 줄이는 데도 도움이 됩니다. ruby.wasm 개발 및 진행 상황은 WebAssembly에서 Ruby로 할 수 있는 작업 강의를 참고하세요.
따라서 Wasm 방정식의 Ruby 부분이 해결됩니다. 하지만 웹 프레임워크로서 Rails에는 이전 다이어그램에 표시된 모든 구성요소가 필요합니다. 다른 구성요소를 브라우저에 배치하고 Rails에서 서로 연결하는 방법을 알아보려면 계속 읽어보세요.
브라우저에서 실행 중인 데이터베이스에 연결
SQLite3에는 공식 Wasm 배포와 상응하는 JavaScript 래퍼가 함께 제공되므로 브라우저에 삽입할 수 있습니다. Wasm용 PostgreSQL은 PGlite 프로젝트를 통해 사용할 수 있습니다. 따라서 Wasm의 Rails 애플리케이션에서 브라우저 내 데이터베이스에 연결하는 방법만 알아내면 됩니다.
데이터 모델링 및 데이터베이스 상호작용을 담당하는 Rails의 구성요소 또는 하위 프레임워크를 Active Record라고 합니다 (예, ORM 디자인 패턴의 이름을 따서 지음). Active Record는 데이터베이스 어댑터를 통해 애플리케이션 코드에서 실제 SQL을 사용하는 데이터베이스 구현을 추상화합니다. Rails는 기본적으로 SQLite3, PostgreSQL, MySQL 어댑터를 제공합니다. 그러나 모두 네트워크를 통해 사용할 수 있는 실제 데이터베이스에 연결한다고 가정합니다. 이를 해결하려면 자체 어댑터를 작성하여 로컬 브라우저 내 데이터베이스에 연결하면 됩니다.
Wasmify Rails 프로젝트의 일부로 구현된 SQLite3 Wasm 및 PGlite 어댑터는 다음과 같이 생성됩니다.
- 어댑터 클래스는 상응하는 기본 제공 어댑터 (예:
class PGliteAdapter < PostgreSQLAdapter
)에서 상속받으므로 실제 쿼리 준비 및 결과 파싱 로직을 재사용할 수 있습니다. - 하위 수준 데이터베이스 연결 대신 JavaScript 런타임에 있는 외부 인터페이스 객체(Rails Wasm 모듈과 데이터베이스 간의 다리)를 사용합니다.
예를 들어 다음은 SQLite3 Wasm의 브리지 구현입니다.
export function registerSQLiteWasmInterface(worker, db, opts = {}) {
const name = opts.name || "sqliteForRails";
worker[name] = {
exec: function (sql) {
let cols = [];
let rows = db.exec(sql, { columnNames: cols, returnValue: "resultRows" });
return {
cols,
rows,
};
},
changes: function () {
return db.changes();
},
};
}
애플리케이션 관점에서 실제 데이터베이스에서 브라우저 내 데이터베이스로 전환하는 것은 구성 문제일 뿐입니다.
# config/database.yml
development:
adapter: sqlite3
production:
adapter: sqlite3
wasm:
adapter: sqlite3_wasm
js_interface: "sqliteForRails"
로컬 데이터베이스를 사용하는 데는 많은 노력이 필요하지 않습니다. 그러나 일부 중앙 정보 소스와의 데이터 동기화가 필요한 경우 더 높은 수준의 문제가 발생할 수 있습니다. 이 질문은 이 게시물의 범위에 해당하지 않습니다 (힌트: PGlite 및 ElectricSQL 데모에서 Rails를 확인하세요).
웹 서버로서의 서비스 워커
웹 애플리케이션의 또 다른 필수 구성요소는 웹 서버입니다. 사용자는 HTTP 요청을 사용하여 웹 애플리케이션과 상호작용합니다. 따라서 탐색 또는 양식 제출에 의해 트리거된 HTTP 요청을 Wasm 모듈로 라우팅하는 방법이 필요합니다. 다행히 브라우저에는 이에 대한 해결책이 있습니다. 바로 서비스 워커입니다.
서비스 워커는 JavaScript 애플리케이션과 네트워크 간의 프록시 역할을 하는 특수한 유형의 웹 워커입니다. 요청을 가로채고 조작할 수 있습니다(예: 캐시된 데이터를 제공하거나 다른 URL 또는 Wasm 모듈로 리디렉션). 다음은 Wasm에서 실행되는 Rails 애플리케이션을 사용하여 요청을 제공하는 서비스의 스케치입니다.
// The vm variable holds a reference to the Wasm module with a
// Ruby VM initialized
let vm;
// The db variable holds a reference to the in-browser
// database interface
let db;
const initVM = async (progress, opts = {}) => {
if (vm) return vm;
if (!db) {
await initDB(progress);
}
vm = await initRailsVM("/app.wasm");
return vm;
};
const rackHandler = new RackHandler(initVM});
self.addEventListener("fetch", (event) => {
// ...
return event.respondWith(
rackHandler.handle(event.request)
);
});
'가져오기'는 브라우저에서 요청할 때마다 트리거됩니다. 요청 정보 (URL, HTTP 헤더, 본문)를 가져와 자체 요청 객체를 구성할 수 있습니다.
Rails는 대부분의 Ruby 웹 애플리케이션과 마찬가지로 HTTP 요청을 처리하기 위해 Rack 인터페이스를 사용합니다. Rack 인터페이스는 요청 및 응답 객체의 형식과 기본 HTTP 핸들러 (애플리케이션)의 인터페이스를 설명합니다. 이러한 속성은 다음과 같이 표현할 수 있습니다.
request = {
"REQUEST_METHOD" => "GET",
"SCRIPT_NAME" => "",
"SERVER_NAME" => "localhost",
"SERVER_PORT" => "3000",
"PATH_INFO" => "/posts"
}
handler = proc do |env|
[
200,
{"Content-Type" => "text/html"},
["<!doctype html><html><body>Hello Web!</body></html>"]
]
end
handler.call(request) #=> [200, {...}, [...]]
요청 형식이 익숙하다면 이전에 CGI를 사용해 본 적이 있을 것입니다.
RackHandler
JavaScript 객체는 JavaScript 렐름과 Ruby 렐름 간에 요청과 응답을 변환합니다. Rack은 대부분의 Ruby 웹 애플리케이션에서 사용되므로 구현은 Rails 전용이 아닌 범용 구현이 됩니다.
하지만 실제 구현은 여기에 게시하기에 너무 깁니다.
서비스 워커는 브라우저 내 웹 애플리케이션의 핵심적인 요소 중 하나입니다. HTTP 프록시일 뿐만 아니라 캐싱 레이어와 네트워크 스위치이기도 합니다. 즉, 로컬 우선 또는 오프라인 지원 애플리케이션을 빌드할 수 있습니다. 또한 사용자가 업로드한 파일을 제공하는 데 도움이 되는 구성요소입니다.
브라우저에 파일 업로드 유지
새 블로그 애플리케이션에서 구현할 첫 번째 추가 기능 중 하나는 파일 업로드 지원, 특히 게시물에 이미지 첨부일 수 있습니다. 이를 위해서는 파일을 저장하고 제공하는 방법이 필요합니다.
Rails에서 파일 업로드를 처리하는 프레임워크의 부분을 Active Storage라고 합니다. Active Storage는 개발자가 하위 수준 저장소 메커니즘을 고려하지 않고도 파일을 작업할 수 있는 추상화 및 인터페이스를 제공합니다. 파일을 하드 드라이브에 저장하든 클라우드에 저장하든 애플리케이션 코드는 이를 인식하지 못합니다.
Active Record와 마찬가지로 맞춤 저장소 메커니즘을 지원하려면 상응하는 저장소 서비스 어댑터를 구현하기만 하면 됩니다. 브라우저에서 파일을 저장하는 위치
기존 옵션은 데이터베이스를 사용하는 것입니다. 예. 데이터베이스에 파일을 blob으로 저장할 수 있으며 추가 인프라 구성요소는 필요하지 않습니다. Rails에는 이미 이를 위한 기성 플러그인인 Active Storage Database가 있습니다. 그러나 WebAssembly 내에서 실행되는 Rails 애플리케이션을 통해 데이터베이스에 저장된 파일을 제공하는 것은 비용이 많이 드는 직렬화(역직렬화) 라운드가 포함되므로 이상적이지 않습니다.
더 나은 브라우저 최적화 솔루션은 File System API를 사용하고 서비스 워커에서 직접 파일 업로드 및 서버 업로드 파일을 처리하는 것입니다. 이러한 인프라에 적합한 후보는 OPFS(출처 비공개 파일 시스템)입니다. OPFS는 향후 브라우저 내 애플리케이션에서 중요한 역할을 할 최신 브라우저 API입니다.
Rails와 Wasm이 함께 달성할 수 있는 목표
이 도움말을 읽기 시작하면서 다음과 같은 질문을 던지셨을 겁니다. 브라우저에서 서버 측 프레임워크를 실행하는 이유는 무엇인가요? 프레임워크 또는 라이브러리가 서버 측 (또는 클라이언트 측)이라는 개념은 라벨일 뿐입니다. 좋은 코드, 특히 좋은 추상화는 어디서나 작동합니다. 라벨이 새로운 가능성을 모색하고 프레임워크 (예: Ruby on Rails)와 런타임 (WebAssembly)의 한계를 뛰어넘는 데 방해가 되어서는 안 됩니다. 두 가지 모두 이러한 색다른 사용 사례의 이점을 누릴 수 있습니다.
기존 또는 실용적인 사용 사례도 많이 있습니다.
첫째, 프레임워크를 브라우저로 가져오면 엄청난 학습 및 프로토타입 제작 기회가 열립니다. 브라우저에서 바로 라이브러리, 플러그인, 패턴을 다른 사용자와 함께 사용할 수 있다고 상상해 보세요. Stackblitz를 사용하면 JavaScript 프레임워크에서 이를 실행할 수 있습니다. 웹페이지를 벗어나지 않고도 WordPress 테마를 사용할 수 있는 WordPress 플레이그라운드도 그 예입니다. Wasm은 Ruby 및 생태계에서 이와 유사한 작업을 지원할 수 있습니다.
오픈소스 개발자에게 특히 유용한 브라우저 내 코딩의 특별한 사례가 있습니다. 문제 분류 및 디버깅입니다. 다시 말해, StackBlitz는 JavaScript 프로젝트에 이 기능을 제공했습니다. 최소 재현 스크립트를 만들고 GitHub Issue의 링크를 가리키면 유지보수자가 시나리오를 재현하는 데 시간을 절약할 수 있습니다. 실제로 RunRuby.dev 프로젝트 덕분에 Ruby에서 이미 시작되었습니다 (브라우저 내 재현으로 해결된 문제 예시).
또 다른 사용 사례는 오프라인 지원 (또는 오프라인 인식) 애플리케이션입니다. 일반적으로 네트워크를 사용하여 작동하지만 연결이 끊겨도 계속 사용할 수 있는 오프라인 지원 애플리케이션입니다. 예를 들어 오프라인 상태에서 받은편지함을 검색할 수 있는 이메일 클라이언트가 있습니다. 또는 '기기에 저장' 기능이 있는 음악 보관함 애플리케이션을 사용하면 네트워크 연결이 없어도 좋아하는 음악을 계속 들을 수 있습니다. 두 예시 모두 기존 PWA와 같이 캐시를 사용하는 것이 아니라 로컬에 저장된 데이터에 종속됩니다.
마지막으로 Rails로 로컬 (또는 데스크톱) 애플리케이션을 빌드하는 것도 좋습니다. 프레임워크가 제공하는 생산성은 런타임에 종속되지 않기 때문입니다. 기능이 완비된 프레임워크는 개인 정보 및 로직이 많은 애플리케이션을 빌드하는 데 적합합니다. Wasm을 휴대용 배포 형식으로 사용하는 것도 좋은 방법입니다.
이는 Wasm 기반 Rails 여정의 시작에 불과합니다. WebAssembly의 Ruby on Rails eBook (오프라인 지원 Rails 애플리케이션 자체임)에서 문제와 해결 방법을 자세히 알아보세요.