Elixir와 BEAM VM이 실시간 앱에 적합한 이유: 경량 프로세스, OTP 감독, 내결함성, Phoenix와 LiveView 기능, 그리고 주요 트레이드오프를 알아봅니다.

“실시간”은 흔히 느슨하게 쓰입니다. 제품 관점에서는 보통 사용자가 페이지를 새로고침하거나 백그라운드 동기화를 기다리지 않고 업데이트를 바로 보는 경험을 말합니다.
실시간은 친숙한 기능들에서 나타납니다:
중요한 것은 *인지되는 즉시성(perceived immediacy)*입니다: 업데이트가 충분히 빨리 도착해서 UI가 라이브하게 느껴지고, 많은 이벤트가 흐를 때도 시스템이 반응성을 유지해야 합니다.
“고동시성”은 앱이 많은 동시 활동을 처리해야 함을 의미합니다—단순히 일시적인 트래픽 급증뿐만 아니라 다음 같은 상황들입니다:
동시성은 진행 중인 독립 작업의 수에 관한 것이지 단순한 초당 요청 수만을 뜻하지 않습니다.
전통적인 연결당 스레드나 무거운 스레드 풀 모델은 한계에 부딪힐 수 있습니다: 스레드는 비교적 비용이 크고, 부하가 증가하면 컨텍스트 스위칭이 늘어나며, 공유 상태 락은 예측하기 어려운 지연을 만들 수 있습니다. 실시간 기능은 또한 연결을 오래 유지하므로 요청 후 자원이 해제되는 대신 누적됩니다.
BEAM 위의 Elixir가 마법은 아닙니다. 여전히 좋은 아키텍처, 합리적인 제한, 신중한 데이터 접근이 필요합니다. 하지만 액터 모델 스타일의 동시성, 경량 프로세스, OTP 관례는 흔한 고통 지점을 줄여주므로 동시성이 증가해도 반응성을 유지하는 실시간 시스템을 더 쉽게 구축할 수 있습니다.
Elixir가 실시간 및 고동시성 앱에서 인기가 있는 이유는 **BEAM 가상 머신(이른바 Erlang VM)**에서 실행되기 때문입니다. 이는 단순한 언어 문법 선택이 아니라, 많은 일이 동시에 일어날 때 시스템의 반응성을 유지하도록 설계된 런타임을 선택하는 것입니다.
BEAM은 통신(telecom) 분야에서 오랜 역사를 가지고 있으며, 그곳의 소프트웨어는 수개월~수년간 최소한의 다운타임으로 실행되는 것이 기대됩니다. 이러한 환경은 Erlang과 BEAM을 예측 가능한 반응성, 안전한 동시성, 시스템 전체를 다운시키지 않고 실패에서 복구하는 능력 쪽으로 밀어넣었습니다.
그 “항상 켜져 있는” 사고방식은 채팅, 라이브 대시보드, 멀티플레이어 기능, 협업 도구, 스트리밍 업데이트 같은 현대적 요구에 직접적으로 연결됩니다—동시에 많은 사용자와 이벤트가 있는 곳이면 어디든 적용됩니다.
동시성을 부가 기능으로 취급하는 대신, BEAM은 많은 독립적 활동을 동시에 관리하도록 구축되었습니다. 작업을 스케줄링하는 방식이 한 바쁜 작업이 다른 모든 것을 얼어붙게 하지 않도록 돕습니다. 결과적으로 시스템은 부하가 걸릴 때에도 요청을 계속 응답하고 실시간 업데이트를 푸시할 수 있습니다.
사람들이 “Elixir 생태계”라고 말할 때 보통 두 가지가 함께 작동하는 것을 의미합니다:
이 조합—Erlang/OTP 위에서 실행되는 Elixir, BEAM 위에서 구동되는 것—이후 섹션들에서 설명할 OTP 감독, Phoenix 실시간 기능의 기반이 됩니다.
Elixir는 BEAM VM 위에서 실행되며, 여기서의 “프로세스” 개념은 운영체제의 프로세스와 매우 다릅니다. 대부분의 사람들이 프로세스나 스레드를 들으면 OS가 관리하는 무거운 단위를 떠올리는데, 그런 것은 하나하나 생성할 때 비용이 크고 신중히 사용해야 합니다.
BEAM 프로세스는 훨씬 가볍습니다: VM이 관리하고, 수천 개(또는 그 이상)로 생성되도록 설계되어 앱이 멈추지 않습니다.
OS 스레드는 바쁜 식당에서 테이블을 예약하는 것과 같습니다: 공간을 차지하고 직원의 주의가 필요하며, 지나가는 모든 사람에게 테이블을 예약할 순 없습니다. BEAM 프로세스는 번호표를 나눠주는 것과 비슷합니다: 나눠주기 싸고 추적하기 쉽고, 모든 사람에게 테이블을 주지 않아도 큰 군중을 관리할 수 있습니다.
실무적으로 BEAM 프로세스는:
프로세스가 저렴하기 때문에 Elixir 앱은 현실 세계의 동시성을 직접적으로 모델링할 수 있습니다:
이 설계는 자연스럽게 느껴집니다: 복잡한 공유 상태와 락을 구축하는 대신 각 "발생하는 일"에 고유한 워커를 부여합니다.
각 BEAM 프로세스는 격리되어 있습니다: 어떤 프로세스가 잘못된 데이터나 예기치 않은 엣지 케이스로 크래시해도 다른 프로세스가 영향을 받지 않습니다. 하나의 문제가 있는 연결이 실패해도 다른 모든 사용자를 오프라인으로 만들지 않습니다.
이 격리는 Elixir가 높은 동시성에서도 버티는 주요 이유 중 하나입니다: 동시 활동 수를 늘리면서도 실패를 국지화하고 회복할 수 있습니다.
Elixir 앱은 많은 스레드가 같은 공유 데이터 구조를 계속 찌르는 방식에 의존하지 않습니다. 대신 많은 작은 프로세스로 작업을 분산하고 메시지 전달로 통신합니다. 각 프로세스가 자신의 상태를 소유하므로 다른 프로세스가 직접 변경할 수 없습니다. 이 단일 설계 선택이 공유 메모리의 많은 문제를 제거합니다.
공유 메모리 동시성에서는 보통 락, 뮤텍스 등으로 상태를 보호합니다. 이는 복잡한 버그를 낳기 쉽습니다: 경쟁 조건, 데드락, "부하에서만 실패"하는 현상 등.
메시지 전달에서는 프로세스가 메시지를 받을 때만 상태를 업데이트하고 메시지를 하나씩 처리합니다. 동일한 가변 메모리에 동시 접근이 없기 때문에 락 순서, 경합, 예측 불가능한 인터리빙을 고민할 시간이 줄어듭니다.
일반적 패턴은 다음과 같습니다:
이것은 실시간 기능에 자연스럽게 매핑됩니다: 이벤트가 들어오면 프로세스가 반응하고, 작업이 분산되어 시스템이 반응성을 유지합니다.
메시지 전달이 과부하를 자동으로 막아주진 않습니다—여전히 백프레셔가 필요합니다. Elixir는 현실적인 옵션을 제공합니다: 경계에서 큐를 제한하거나(메일박스 크기 제한), 명시적 플로우 제어를 적용하거나 파이프라인 스타일 도구로 처리량을 조절할 수 있습니다. 요점은 공유 상태 복잡성을 도입하지 않고도 이러한 제어를 프로세스 경계에 추가할 수 있다는 것입니다.
사람들이 “Elixir는 내결함성이 있다”고 말할 때 보통 OTP를 의미합니다. OTP는 하나의 마법 라이브러리가 아니라 검증된 패턴과 빌딩 블록(behaviours, 설계 원칙, 도구 모음)으로, 장기 실행 시스템을 구조화해 우아하게 복구하도록 돕습니다.
OTP는 작업을 작고 격리된 책임 단위의 프로세스로 분할하도록 권장합니다. 한 번도 실패하지 않아야 하는 거대한 서비스 대신 많은 작은 워커로 시스템을 구성하면 개별 실패가 전체를 무너뜨리지 않습니다.
자주 보는 워커 유형:
슈퍼바이저는 다른 프로세스(워커)를 시작, 모니터, 재시작하는 역할을 하는 프로세스입니다. 워커가 크래시하면(잘못된 입력, 타임아웃, 일시적 의존성 문제 등) 슈퍼바이저는 선택한 전략에 따라 자동으로 재시작할 수 있습니다(개별 워커 재시작, 그룹 재시작, 반복 실패 시 백오프 등).
이것이 **감독 트리(supervision tree)**를 만들며, 실패가 국한되고 복구가 예측 가능해집니다.
“Let it crash”는 모든 워커 내부에 방대한 방어 코드를 넣지 않는다는 뜻입니다:
결과는 개별 컴포넌트가 잘못 동작해도 시스템 전체가 계속 사용자에 응답하는 것입니다—실시간 고동시성 앱에서 바라는 바입니다.
제품 맥락에서의 "실시간"은 보통 소프트 실시간입니다: 사용자는 시스템이 충분히 빨리 반응해서 즉각적인 느낌을 받기를 기대합니다—채팅 메시지가 바로 표시되고, 대시보드가 부드럽게 갱신되며, 알림이 1~2초 이내에 도착하는 정도. 가끔 느려질 수는 있지만, 지연이 자주 발생하면 신뢰가 떨어집니다.
Elixir는 BEAM VM 위에서 실행되며, 많은 작고 격리된 프로세스 중심으로 설계되어 있습니다. 핵심은 BEAM의 **선점형 스케줄러(preemptive scheduler)**입니다: 작업을 매우 작은 시간 단위로 나누어 한 코드 조각이 오랫동안 CPU를 독점하지 못하게 합니다. 수천~수백만의 동시 활동(웹 요청, WebSocket 푸시, 백그라운드 작업)이 있을 때도 스케줄러는 이들을 순환시키며 각자 차례를 줍니다.
이것이 트래픽 급증 시에도 Elixir 시스템이 보통 "반응이 빠른" 느낌을 유지하는 주요 이유입니다.
많은 전통 스택은 OS 스레드와 공유 메모리에 크게 의존합니다. 고동시성 하에서는 스레드 경합이 발생할 수 있습니다: 락, 컨텍스트 스위칭 오버헤드, 요청이 쌓이는 큐잉 효과 등. 결과는 평균은 괜찮아 보여도 무작위로 발생하는 수초 단위의 꼬리 지연(p99)으로 이어집니다.
BEAM 프로세스는 메모리를 공유하지 않고 메시지 전달로 통신하므로 이러한 병목 중 다수를 회피할 수 있습니다. 물론 좋은 아키텍처와 용량 계획이 필요하지만, 런타임은 부하가 늘어날 때 지연을 더 예측 가능하게 유지하도록 도와줍니다.
소프트 실시간은 Elixir에 잘 맞습니다. 하드 실시간—기한을 놓치는 것이 용납되지 않는 영역(의료 장비, 비행 제어, 특정 산업 제어기 등)—은 보통 특수 운영체제, 언어, 검증 기법을 필요로 합니다. Elixir는 그 생태계와 연동할 수 있지만 엄격한 기한 보장이 필요한 핵심 도구로 쓰이진 않습니다.
Phoenix는 Elixir 위에서 실시간 계층으로 자주 선택되는 프레임워크입니다. 수천 클라이언트가 동시에 연결되어도 라이브 업데이트를 단순하고 예측 가능하게 유지하도록 설계되었습니다.
Phoenix Channels는 WebSocket(또는 롱폴링 폴백)을 구조화된 방식으로 사용할 수 있게 해줍니다. 클라이언트는 주제(topic)에 조인(예: room:123)하고 서버는 해당 주제의 모두에게 이벤트를 푸시하거나 개별 메시지에 응답할 수 있습니다.
직접 만든 WebSocket 서버와 달리 Channels는 깔끔한 메시지 기반 흐름(조인, 이벤트 처리, 브로드캐스트)을 장려하여 채팅, 라이브 알림, 협업 편집 같은 기능이 콜백의 얽힘으로 변하는 것을 막습니다.
Phoenix PubSub는 앱의 일부가 이벤트를 발행하고 다른 부분이 구독하도록 하는 내부의 브로드캐스트 버스입니다—로컬 또는 확장 시 노드 간에도 동작합니다.
실시간 업데이트는 보통 소켓 프로세스 자체에서 직접 트리거되지 않습니다. 결제가 완료되거나 주문 상태가 바뀌거나 댓글이 추가되면 PubSub가 그 변경을 모든 관심 있는 구독자(채널, LiveView 프로세스, 백그라운드 잡)에 브로드캐스트하게 해 느슨한 결합을 유지합니다.
Presence는 누가 연결되어 있고 무엇을 하고 있는지 추적하는 Phoenix의 내장 패턴입니다. 온라인 사용자 목록, 타이핑 인디케이터, 액티브 편집자 표시 등에 흔히 사용됩니다.
간단한 팀 채팅에서는 각 방을 room:42 같은 주제로 만들 수 있습니다. 사용자가 메시지를 보내면 서버는 이를 영속화한 뒤 PubSub로 브로드캐스트하여 연결된 모든 클라이언트가 즉시 보게 합니다. Presence는 누가 방에 있는지와 누가 입력 중인지 보여주고, notifications:user:17 같은 별도 토픽은 실시간으로 멘션 알림을 푸시할 수 있습니다.
Phoenix LiveView는 대부분의 로직을 서버에 두고 지속 연결(대개 WebSocket)을 통해 작은 UI 업데이트만 전송해 인터랙티브하고 실시간인 UI를 만듭니다. 브라우저가 이 변경을 즉시 적용하므로 많은 클라이언트 측 상태를 직접 관리하지 않아도 페이지가 라이브하게 느껴집니다.
진실의 근원이 서버에 남아 있으므로 많은 전형적 클라이언트 앱의 함정을 피할 수 있습니다:
LiveView는 데이터 변경 시 테이블을 업데이트하거나 진행 상황을 실시간으로 표시하거나 프레즌스를 반영하는 등의 실시간 기능을 서버 렌더 흐름의 일부로 간단히 처리하게 합니다.
LiveView는 관리 패널, 대시보드, 내부 도구, CRUD 앱, 폼 중심 워크플로우에 탁월합니다. 자바스크립트 부담을 적게 하고도 현대적 인터랙티브 경험을 원할 때 좋은 선택입니다.
제품이 오프라인 우선 동작, 연결이 끊긴 상태에서의 광범위한 작업, 또는 고도로 커스텀한 클라이언트 렌더링(복잡한 캔버스/WebGL, 무거운 클라이언트 애니메이션, 네이티브에 가까운 상호작용)을 필요로 한다면 풍부한 클라이언트 앱(또는 네이티브)을 선택하고 백엔드는 Phoenix를 실시간/API 백엔드로 사용할 수 있습니다.
실시간 Elixir 앱을 확장할 때 보통 묻는 질문은: 같은 애플리케이션을 여러 노드에서 실행해 하나의 시스템처럼 동작하게 할 수 있는가? BEAM 기반 클러스터링으로 답은 종종 “예”입니다—동일한 노드를 여러 대 띄워 클러스터로 연결하고 로드 밸런서를 통해 트래픽을 분산하면 됩니다.
클러스터는 서로 통신할 수 있는 Elixir/Erlang 노드들의 집합입니다. 연결되면 메시지를 라우팅하고 작업을 조율하며 일부 서비스를 공유할 수 있습니다. 프로덕션에서는 보통 서비스 디스커버리(Kubernetes DNS, Consul 등)를 통해 노드가 자동으로 서로를 찾도록 합니다.
실시간 기능에서 분산 PubSub는 중요합니다. Phoenix에서는 노드 A에 연결된 사용자가 노드 B에서 발생한 업데이트를 받아야 할 때 PubSub가 그 다리가 됩니다: 브로드캐스트가 클러스터 전반에 복제되어 각 노드가 자체 연결된 클라이언트에게 푸시할 수 있습니다.
이로써 진정한 수평 확장이 가능해집니다: 노드를 추가하면 동시 연결 수와 처리량이 증가하면서도 실시간 전달이 깨지지 않습니다.
Elixir는 프로세스 내부에 상태를 유지하는 것을 쉽게 해주지만, 확장하면 의도적으로 설계해야 합니다:
대부분 팀은 릴리스(보통 컨테이너 내)를 사용해 배포합니다. 헬스 체크(liveness/readiness)를 추가하고, 노드가 발견·연결될 수 있게 하며, 롤링 배포에서 노드가 클러스터에 조인/퇴장해도 전체 시스템이 끊기지 않도록 계획해야 합니다.
Elixir는 많은 동시적 "작은 대화"가 동시에 일어나는 제품에 적합합니다—많은 연결 클라이언트, 빈번한 업데이트, 그리고 일부 시스템이 오작동해도 계속 응답해야 하는 경우.
채팅 및 메시징: 수천~수백만의 장기 연결이 흔합니다. 경량 프로세스 모델이 사용자/룸당 한 프로세스 패턴과 자연스럽게 맞아 팬아웃(fan-out)을 반응적으로 유지합니다.
협업(문서, 화이트보드, 프레즌스): 실시간 커서, 타이핑 인디케이터, 상태 동기화가 지속적으로 일어납니다. Phoenix PubSub와 프로세스 격리가 락의 엉킴 없이 효율적으로 브로드캐스트하도록 돕습니다.
IoT 수집 및 텔레메트리: 장치들이 작은 이벤트를 지속적으로 보내고 트래픽이 급증할 수 있습니다. Elixir는 높은 연결 수와 백프레셔 친화적 파이프라인을 잘 처리하며 OTP 감독은 다운스트림 의존성이 실패할 때 예측 가능한 복구를 제공합니다.
게임 백엔드: 매치메이킹, 로비, 게임별 상태는 많은 동시 세션을 수반합니다. Elixir는 빠른 동시 상태 머신(종종 "매치당 한 프로세스")을 지원하고 버스트 동안 꼬리 지연을 제어할 수 있습니다.
금융 알림 및 통지: 신뢰성이 속도만큼 중요합니다. Elixir의 내결함성 설계와 감독 트리는 외부 서비스 타임아웃 시에도 시스템이 계속 처리하도록 돕습니다.
다음 질문을 해보세요:
초기부터 목표를 정의하세요: 처리량(events/sec), 지연(p95/p99), 그리고 에러 버짓(허용 가능한 실패율). Elixir는 이런 목표가 엄격하고 부하하에서도 충족해야 할 때 빛나는 경우가 많습니다.
Elixir는 많은 동시적이고 주로 I/O 바운드 작업을 처리하는 데 탁월합니다—WebSocket, 채팅, 알림, 오케스트레이션, 이벤트 처리 등. 하지만 모든 경우에 최선은 아닙니다. 트레이드오프를 알면 Elixir를 최적이 아닌 문제에 억지로 쓰는 일을 피할 수 있습니다.
BEAM은 반응성과 예측 가능한 지연을 우선시하므로 원시 CPU 처리량에서 최적이 아닐 수 있습니다—비디오 인코딩, 무거운 수치 연산, 대규모 ML 트레이닝 등은 다른 생태계가 나을 수 있습니다.
Elixir 시스템에서 CPU 집약 작업이 필요하면 일반적 접근법은:
Elixir 자체는 접근성이 좋지만 OTP 개념(프로세스, 슈퍼바이저, GenServer, 백프레셔 등)은 내재화하는 데 시간이 걸립니다. 요청/응답 웹 스택에서 온 팀은 "BEAM 방식"의 설계를 익히는 데 러닝 커브가 필요합니다.
지역에 따라 채용이 더디게 느껴질 수 있습니다. 많은 팀이 내부 교육이나 멘토링을 계획합니다.
핵심 도구는 강력하지만 일부 분야(특정 엔터프라이즈 통합, 틈새 SDK)는 Java/.NET/Node보다 성숙한 라이브러리가 적을 수 있습니다. 더 많은 글루 코드나 래퍼를 작성해야 할 수 있습니다.
단일 노드 운영은 간단하지만 클러스터링은 복잡도를 추가합니다: 디스커버리, 네트워크 분할, 분산 상태, 배포 전략. 관찰성은 좋지만 추적, 메트릭, 로그 연관을 위해 의도적인 설정이 필요할 수 있습니다. 조직이 최소한의 커스터마이징으로 완성형 운영을 원하면 더 일반적인 스택이 단순할 수 있습니다.
만약 앱이 실시간이 아니고 동시성도 높지 않으며 주로 적당한 트래픽의 CRUD라면 팀이 이미 아는 주류 프레임워크를 선택하는 쪽이 빠른 길일 수 있습니다.
Elixir 도입은 큰 리라이트일 필요가 없습니다. 가장 안전한 경로는 작게 시작해 한 실시간 기능으로 가치를 증명한 뒤 확장하는 것입니다.
실용적 첫 단계로 작은 Phoenix 앱을 만들어 실시간 동작을 보여주세요:
범위를 좁게 유지하세요: 한 페이지, 한 데이터 소스, 명확한 성공 지표(예: "1,000명 연결 시 200ms 이내에 업데이트 표시"). 설정과 개념 개요가 필요하면 /docs에서 시작하세요.
제품 경험을 검증하기 전이라면 주변 UI와 워크플로를 빠르게 프로토타입하는 것도 도움이 됩니다. 예를 들어 팀들은 종종 Koder.ai 같은 도구로 프론트엔드(React)와 백엔드(Go + PostgreSQL)를 빠르게 스케치한 뒤, 요구사항이 명확해지면 Elixir/Phoenix 실시간 컴포넌트를 통합하거나 교체합니다.
작은 프로토타입에서도 작업을 격리된 프로세스(사용자당, 룸당, 스트림당)에서 수행되도록 구조화하세요. 이렇게 하면 무엇이 어디서 실행되고 실패 시 무슨 일이 일어나는지 이해하기 쉬워집니다.
초기부터 **감독(supervision)**을 추가하세요. 핵심 워커를 슈퍼바이저 아래에 두고 재시작 동작을 정의하며, 하나의 "메가 프로세스"보다 작은 워커를 선호하세요. 이것이 Elixir가 다른 점입니다: 실패가 일어날 것을 가정하고 회복 가능하게 만듭니다.
기존 시스템이 다른 언어로 되어 있다면 일반적 마이그레이션 패턴은:
기능 플래그를 사용하고 Elixir 컴포넌트를 병행 실행하며 지연과 오류율을 모니터링하세요. 프로덕션 사용 계획이나 지원을 평가 중이라면 /pricing을 확인하세요.
만약 벤치마크, 아키텍처 노트, 튜토리얼을 공유한다면 Koder.ai의 earn-credits 프로그램을 통해 콘텐츠 생성이나 추천으로 도구 비용을 상쇄하는 것도 유용할 수 있습니다.
"실시간"은 대부분의 제품 문맥에서 소프트 실시간을 의미합니다: 업데이트가 UI에 즉시 반영되는 느낌을 주는 수준(대개 수백 밀리초에서 1~2초 이내)이며 수동 새로고침이 필요 없습니다.
이는 미리 정해진 기한을 절대적으로 지켜야 하는 하드 실시간과는 다릅니다. 하드 실시간은 일반적으로 특수한 운영체제와 검증 기법을 요구합니다.
고동시성(High concurrency)은 동시에 진행되는 독립적 활동의 수에 관한 것입니다. 단순히 초당 요청 수가 많은 것과는 다릅니다.
예시:
연결당 스레드(thread-per-connection) 설계는 스레드가 비교적 무겁고 동시성이 커지면 오버헤드가 증가하기 때문에 문제를 겪기 쉽습니다.
일반적인 문제점:
BEAM 프로세스는 VM이 관리하는 경량 단위로, 매우 많은 수로 생성되도록 설계되었습니다.
실제로 이는 “연결/사용자/작업당 한 프로세스” 같은 패턴을 현실적으로 적용할 수 있게 해주며, 복잡한 공유 상태 락 없이 실시간 시스템을 모델링하기 쉽습니다.
메시지 전달 방식에서는 각 프로세스가 자신의 상태를 소유하고, 다른 프로세스는 메시지를 보내 상호작용합니다.
이 방식은 전통적 공유 메모리에서 발생하는 문제를 줄여줍니다:
부하 급증 시 백프레셔는 프로세스 경계에서 구현할 수 있어 시스템이 갑자기 무너지지 않고 점진적으로 열화를 겪게 합니다.
자주 쓰이는 기법:
OTP는 장기 실행 시스템에서 실패를 회복하도록 돕는 관례와 빌딩 블록 모음입니다.
핵심 요소:
“Let it crash”는 오류를 무시한다는 뜻이 아닙니다. 각 워커에 과도한 방어 코드를 넣기보다 실패 시 슈퍼바이저가 깔끔한 상태로 복구하도록 설계하는 접근입니다.
실무적으로:
Phoenix의 실시간 기능은 보통 세 가지 도구로 구성됩니다:
LiveView는 대부분의 UI 상태와 로직을 서버에 두고, 지속 연결을 통해 작은 변경만 브라우저에 전송합니다.
이 방식은 다음에 잘 맞습니다:
오프라인 우선 앱이나 캔버스/WebGL 같은 고도로 커스텀한 클라이언트 렌더링에는 적합하지 않을 수 있습니다.