Hotwire는 요즘 모든 Rails 개발자들 사이에서 가장 뜨거운 화두입니다. Rails로 개발하고 계신다면 이미 여러 차례 들어보셨을 가능성이 높습니다.
Hotwire는 단 몇 줄의 코드만으로 애플리케이션에 인터랙티브 기능을 추가할 수 있는 완전히 새로운 방식입니다. HTML을 네트워크(over the wire)로 직접 전송하는 방식을 채택하고 있어 놀라울 정도로 빠르게 동작합니다. 덕분에 대부분의 SPA(싱글 페이지 애플리케이션) 프레임워크 없이도 개발할 수 있으며, 렌더링 로직을 서버 쪽에 집중시키면서도 빠른 페이지 로딩 속도와 뛰어난 인터랙티브 경험을 동시에 유지할 수 있습니다.
이 글에서는 Hotwire의 핵심 구성 요소와 Rails 앱에서 이를 활용하는 방법을 살펴보겠습니다. 그 전에 먼저, Hotwire란 정확히 무엇이고 왜 사용해야 할까요?
Hotwire란 무엇인가?
Hotwire는 단일 라이브러리가 아니라, HTML을 네트워크를 통해 전송하는 방식으로 웹·모바일 애플리케이션을 구축하는 새로운 접근 방식입니다. 크게 Turbo, Stimulus, Strada(올해 말 출시 예정) 세 가지로 구성되어 있으며, 각각의 자세한 내용은 아래에서 다룹니다.
참고: Hotwire는 Rails와 밀접하게 연관되어 있지만, 특정 언어나 프레임워크에 종속되지 않기 때문에 다른 환경에서도 충분히 활용할 수 있습니다. 필자 역시 여러 비(非)Rails 애플리케이션과 일부 정적 웹사이트에서 Stimulus를 프로덕션 환경에 적용해 사용해 왔으며, Turbo 역시 Rails 없이 사용 가능합니다.
하지만 지금은 다시 Rails의 세계로 돌아가 보겠습니다.
Rails 앱에서 Hotwire를 사용해야 하는 이유
그렇다면 언제 Hotwire를 사용해야 할까요? 답은 간단합니다. 애플리케이션에 인터랙티브 기능을 추가하고 싶다면 어디서든 활용할 수 있습니다. 예를 들어 다음과 같은 경우가 대표적입니다.
- 사용자 상호작용에 따라 특정 콘텐츠를 조건부로 표시하거나 숨기고 싶을 때 (예: 국가 선택에 따라 시/도 목록이 자동으로 바뀌는 주소 입력 폼)
- 콘텐츠를 실시간으로 갱신하고 싶을 때 (예: 새 트윗이 페이지 새로고침 없이 자동으로 추가되는 트위터 같은 피드)
- 페이지의 일부를 지연 로딩(lazy-load)하고 싶을 때 (예: 아코디언 컴포넌트에서 제목만 먼저 로드하고 상세 내용은 나중에 불러와 초기 로딩 속도를 개선)
Hotwire의 구성 요소
앞서 언급했듯이 Hotwire는 웹 앱을 만들기 위한 새롭고 검증된 기술들의 모음입니다. 지금부터 각 구성 요소를 하나씩 자세히 살펴보겠습니다.
Turbo
Turbo의 핵심은 HTML입니다. Turbo는 네트워크를 통해 전달된 HTML 데이터를 처리하고, 전체 페이지를 다시 불러오지 않고도 애플리케이션에 표시할 수 있는 다양한 기법을 제공합니다. 다음 네 가지로 구성됩니다.
-
Turbo Drive
과거 Turbolinks를 사용해 보셨다면 Turbo Drive를 매우 익숙하게 느끼실 겁니다. 핵심 원리는 JavaScript 코드가 애플리케이션의 이벤트를 가로채어 HTML을 비동기적으로 불러오고, 마크업의 일부를 교체하는 것입니다.
-
Turbo Frames
Turbo Frames는 마크업을 독립적으로 로드할 수 있는 여러 섹션으로 분리해 줍니다. 예를 들어 블로그 애플리케이션에서 게시물 본문과 댓글은 서로 연관되어 있지만 독립적인 영역입니다. 이 둘을 분리하면 각각 독립적으로 탐색하거나 turbo frames를 통해 비동기적으로 로드할 수 있습니다.
-
Turbo Streams
Turbo Streams는 실시간 데이터를 애플리케이션에 손쉽게 반영할 수 있는 유틸리티를 제공합니다. 예를 들어 트위터처럼 뉴스 피드를 만든다고 가정해 보겠습니다. 새 트윗이 게시되는 즉시 페이지를 새로고침하지 않고도 사용자의 피드에 반영하고 싶을 것입니다. Turbo Streams를 사용하면 JavaScript를 한 줄도 작성하지 않고 이를 구현할 수 있습니다.
-
Turbo Native
Turbo Native를 사용하면 웹 애플리케이션을 감싸는 네이티브 래퍼를 만들 수 있습니다. 모든 화면을 네이티브로 다시 만들 필요 없이, 탐색과 상호작용이 네이티브 앱처럼 느껴지도록 할 수 있습니다. 나머지 부분은 웹을 통해 계속 제공하면 되므로, 정말 인터랙티브한 핵심 부분에만 집중할 수 있습니다.
Stimulus
Stimulus는 HTML과 상호작용하는 컨트롤러를 작성하기 위한 JavaScript 프레임워크입니다.
페이지의 요소에 data-controller, data-action, data-target 같은 JavaScript 속성을 추가한다고 가정해 보겠습니다. 이 속성들을 기반으로 요소에 접근하고 이벤트를 수신하는 Stimulus 컨트롤러를 작성하면 됩니다.
연결된 Stimulus 컨트롤러 코드를 읽지 않아도 이 HTML이 어떤 동작을 하는지 아주 쉽게 파악할 수 있다는 점이 매력적입니다.
이 HTML과 함께 사용되는 컨트롤러는 다음과 같습니다.
이것이 바로 Stimulus의 핵심 철학입니다. 단순함을 유지하고 재사용성을 높이는 것입니다. 이제 다른 페이지에서 클립보드 복사 버튼이 필요하다면 해당 컨트롤러를 그대로 재사용하면 됩니다. 마크업에 data-* 속성만 추가하면 모든 것이 동작합니다.
Strada
아쉽지만 현재 Strada에 대해 공개된 정보는 많지 않습니다. 다만 웹 애플리케이션이 HTML 브릿지 속성을 통해 네이티브 앱과 통신하고(필요하다면 액션까지 수행하고) 상호작용할 수 있게 해줄 것으로 알려져 있습니다.
Ruby on Rails 앱에서 Hotwire 활용하기
설치 과정이나 기본 사용 사례에 시간을 들이기보다는 바로 실전으로 넘어가겠습니다. 설치 관련 자세한 내용은 Hotwire 팀의 공식 스크린캐스트와 turbo-rails, Stimulus 설치 문서를 참고하시기 바랍니다.
자, 이제 Hotwire의 대표적인 활용 사례를 살펴보겠습니다.
무한 스크롤(Endless Scroll)
Turbo Frames를 활용하면 사용자가 스크롤할 때 자동으로 페이지네이션이 되는 화면을 손쉽게 만들 수 있습니다. 이를 위해 두 가지만 하면 됩니다.
- 프레임 id에 페이지 번호를 붙여 각 '페이지'를 자체 프레임 안에 렌더링합니다. (예:
turbo_frame_tag "posts_#{@posts.current_page}") - 다음 페이지에는
lazy프레임을 사용해, 화면에 나타나기 전까지는 자동으로 로드되지 않도록 합니다.
이 예제는 Kaminari의 메서드를 사용하지만, 다른 페이지네이션 방식으로도 손쉽게 변형할 수 있습니다.
컨트롤러에는 특별한 코드가 필요하지 않습니다. 표준적인 index 메서드면 충분합니다.
핵심 트릭은 중첩 프레임(nested frames)을 사용한다는 점입니다. 다음 페이지의 프레임이 이전 페이지의 프레임 안에 중첩되어 있습니다. 이렇게 하면 첫 페이지가 로드될 때 다음 페이지의 프레임이 맨 끝에 위치하게 되고, 사용자가 그 프레임까지 스크롤하면 해당 프레임이 두 번째 페이지의 콘텐츠로 교체됩니다. 그리고 세 번째 페이지의 lazy 프레임이 다시 맨 끝에 렌더링되는 방식입니다.
동적 폼(Dynamic Forms)
Hotwire를 사용하면 프런트엔드에서 필드를 토글하는 별도의 커스텀 로직 없이도 동적 폼을 쉽게 구현할 수 있습니다. 무한 스크롤 사례보다는 다소 복잡하지만, Turbo Stream과 Stimulus를 함께 활용하는 좋은 예입니다.
먼저 폼부터 살펴보겠습니다.
폼 자체는 간단합니다. News와 Blog 옵션을 가진 kind 셀렉트 박스를 표시합니다. 선택된 종류에 따라 사용 가능한 카테고리 값이 변경되기를 원합니다 (categories_for_kind(@post.kind)가 해당 종류의 카테고리 목록을 반환한다고 가정).
자세히 보면 폼에 몇 가지 data 속성이 추가된 것을 확인할 수 있습니다. data-target은 폼 요소를 RefreshFormController Stimulus 컨트롤러의 form 타깃과 연결합니다. 그리고 change->refresh-form#refreshForm 값을 가진 data-action은 kind 셀렉트 박스가 변경될 때마다 연결된 Stimulus 컨트롤러의 refreshForm 메서드를 호출합니다.
이제 Stimulus 컨트롤러를 살펴보겠습니다.
refreshForm이 호출되면 컨트롤러 URL(data-controller="refresh-form"이 설정된 동일 요소의 data-refresh-form-url로 지정)로 새로운 PUT 요청을 보냅니다. 여기서 중요한 점은 responseKind가 turbo-stream으로 설정되어 있다는 것입니다. @rails/request 라이브러리는 이 응답을 이해하고 응답 스트림의 지시에 따라 동작을 수행합니다.
이제 Turbo가 폼을 이해하고 갱신할 수 있도록 refresh_form 호출에서 올바른 스트림을 반환하는 일만 남았습니다.
게시물의 속성을 업데이트하고 turbo_stream 형식으로 응답하겠다고 표시하면 됩니다 (그러면 refresh_form.turbo_stream.erb 파일을 찾게 됩니다).
이 단계에서는 기존의 form 파셜(partial)을 재사용하여, replace 액션을 가진 turbo_stream으로 감싸고 있습니다.
이것으로 동적 폼 구현이 완료되었습니다. 다소 고급 기술처럼 보일 수 있지만, refresh Stimulus 컨트롤러는 공유 가능한 부품이므로 이후에는 적절한 data-* 속성만 추가하면 어떤 동적 폼에도 사용할 수 있습니다. 다시 말해, 다른 폼을 위해 새로운 JS를 작성하지 않고도 서버 측 동적 폼 갱신 기능을 얻게 되는 것입니다. 꽤 멋지지 않나요?
페이지 새로고침 없이 콘텐츠 추가하기
Hotwire가 쉽게 만들어 주는 다음 활용 사례는 WebSocket 연결을 통해 HTML을 스트리밍하고, 새 콘텐츠가 도착하면 페이지를 실시간으로 갱신하는 것입니다. GitHub의 댓글 섹션이 좋은 예입니다. Turbo Streams를 사용하면 이를 매우 쉽게 구현할 수 있습니다.
구현은 두 단계로 이루어집니다.
먼저, 목록 페이지에 turbo stream 리스너를 삽입하여 서버와 WebSocket 연결을 열고 이벤트를 수신 대기합니다.
다음으로, 모델을 수정하여 새 댓글이 스트림으로 브로드캐스트되도록 합니다.
이것이 전부입니다. 새 댓글마다 Turbo가 자동으로 app/views/comments/_comment.html.erb 파셜을 렌더링하여 WebSocket 연결을 통해 전송합니다. 이는 Turbo의 JS가 수신하여 id가 comments인 타깃 앞에 추가됩니다.
한 걸음 더 나아가, 작은 Stimulus 컨트롤러로 새로 추가된 댓글에 하이라이트 표시를 달아 보겠습니다.
먼저, 브로드캐스트와 comment 파셜을 수정하여 컨트롤러를 조건부로 포함시킵니다.
이 작은 Stimulus 컨트롤러는 연결 시 특별한 하이라이트 클래스를 3초간 추가한 뒤 제거합니다.
참고: 해당 클래스의 유무에 따라 CSS 하이라이트 스타일도 함께 정의해야 합니다.
이 컨트롤러가 완성되면 하이라이트 클래스가 필요한 어느 곳에서든 재사용할 수 있습니다. 더 많은 유연성이 필요하다면 지속 시간과 클래스 이름을 data 속성에서 가져오도록 수정할 수도 있습니다.
이것이 바로 Hotwire의 위력입니다. JS에 손을 대지 않고도 상당히 먼 길까지 갈 수 있으며, 정말 JS가 필요한 순간에도 Stimulus가 재사용 가능한 범용 컨트롤러를 만들 수 있는 도구를 제공해 줍니다.
마치며, 그리고 더 읽을 거리
Rails 커뮤니티는 Hotwire의 등장에 크게 흥분했고, 그럴 만한 이유가 충분합니다.
이 글에서는 Hotwire의 핵심 구성 요소와 Rails 앱에서의 활용 방법을 살펴보았습니다. Turbo와 Stimulus를 사용해 애플리케이션에 생기를 불어넣는 방법도 함께 다루었습니다.
공식 Hotwire 스크린캐스트 소개와 Turbo 문서는 Hotwire와 Turbo가 무엇을 해줄 수 있는지 확인하기에 가장 좋은 시작점입니다.
고급 활용법을 원하신다면 turbo-rails GitHub 저장소를 방문해 보시길 권합니다. 아쉽게도 문서화가 다소 부족하지만, 코드를 파는 것을 두려워하지 않으신다면 다음 파일들의 코드와 인라인 주석을 읽어보세요.
- Turbo Frames 관련:
Turbo::FramesHelper - 코드에서 Turbo Streams로 브로드캐스트:
Turbo::Broadcastable - 컨트롤러 액션 내에서 Turbo Streams로 브로드캐스트:
Turbo::Streams::TagBuilder
즐거운 코딩 되세요!
P.S. Ruby Magic의 글을 발행 즉시 읽고 싶으시다면 Ruby Magic 뉴스레터를 구독하고 어떤 글도 놓치지 마세요!
Sapan Diwakar
게스트 저자 사판 디왁카르(Sapan Diwakar)는 풀스택 개발자입니다. 자신의 블로그에서 관심사에 대한 글을 쓰고 있으며, 삶과 코드 모두에서 단순함을 유지하는 것을 진심으로 좋아합니다. 기술 외적인 시간에는 정원을 가꾸고, 숲속을 하이킹하며, 야외 스포츠를 즐기는 것을 사랑합니다.