Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Rails에서 Trix와 Turbo 프레임으로 동적 테이블 편집기 만들기

이번 포스트에서는 Rails 애플리케이션에 기본적인 ActionText 테이블 편집기를 구현해 보겠습니다. 이 과정에서 다음 내용들을 배우게 됩니다.

  • ActionText와 Trix가 첨부(attachment)를 처리하는 방식
  • 자체 Attachable 타입을 구현하고 이를 활용해 기본 테이블 편집기를 만드는 방법
  • Turbo Frame으로 테이블을 편집하는 방법
  • Turbo가 도움이 되면서 동시에 걸림돌이 되는 지점

이 글은 2020년에 발표된 훌륭한 블로그 포스트 'Adding Tables to ActionText With Stimulus.js'에서 영감을 얻었습니다. 다만 해당 글은 Turbo가 등장하기 전에 작성된 것이며, Turbo를 활용하면 문제가 상당히 단순해질 것으로 기대할 수 있습니다.

그럼 시작해 보겠습니다!

Rails 101: ActionText 첨부(Attachment)의 기초

참고: 이 데모는 Trix와 Turbo Frame에 대한 기본적인 이해를 전제로 합니다. Hotwire와 Turbo Frame의 기초를 익히고 싶다면 'Get Started with Hotwire in Your Ruby on Rails App' 포스트가 도움이 될 것입니다.

GitHub 저장소를 통해 코드 데모를 따라 해볼 수도 있습니다.

ActionText 공식 문서에는 다음과 같이 설명되어 있습니다.

Action Text는 Rails에 리치 텍스트 콘텐츠와 편집 기능을 제공합니다. 서식 지정부터 링크, 인용구, 목록, 이미지 및 갤러리 삽입까지 모든 것을 처리하는 Trix 에디터가 포함되어 있습니다.

큰 그림으로 보면, 첨부 파일은 ActionText 문서 모델의 일부입니다. ActionText는 Signed Global ID(SGID)로 식별 가능한 모든 리소스에 대해 사용자 정의 템플릿을 렌더링합니다. 즉, ActionText는 특정 SGID에 대한 참조를 <action-text-attachment> 요소 형태로 저장합니다.


ActionText가 이런 요소를 만나면 해당 리소스의 to_attachable_partial_path 메서드를 호출합니다. 기본적으로 이 메서드는 to_partial_path에 위임됩니다.

미리 살펴보자면, Table 객체가 ActionText에서 HTML로 렌더링될 때는 아래와 같은 모습이 될 것입니다.


ActionText Attachment API를 준수하기 위해 클래스가 해야 할 일은 딱 두 가지입니다.

  1. GlobalID::Identification을 포함하여 to_sgid를 구현합니다. 기본적으로 모든 ActiveRecord::Base 하위 클래스는 이미 이를 갖추고 있습니다.
  2. ActionText::Attachable 모듈을 include 합니다.

ActionText::Attachable 모듈은 attachable_sgidfrom_attachable_sgid 메서드를 통해 어떤 모델이든 SGID로 변환하거나 되돌리는 표준 방법을 제공합니다. 이 기능은 뒤에서 실제로 활용하게 됩니다.

또한 파일 크기, 파일명, 콘텐츠 유형(content type) 같은 첨부 메타데이터에 대한 편리한 접근자도 함께 제공합니다.

마지막으로, 에디터와 리치 텍스트 뷰에서 첨부 파일을 렌더링하는 데 사용되는 파셜(partial)의 기본 경로도 제공합니다.

테이블 모델 추가하기

우리는 ActionText의 Attachment API를 활용해 테이블 솔루션을 구현할 것입니다. 이를 위해서는 테이블 데이터를 담는 커스텀 모델을 만들고 Attachable을 include해야 합니다. 테이블 데이터는 간단한 JSON(B) 컬럼에 2차원 배열 형태로 저장하겠습니다.

먼저 ActionText가 활성화된 새 Rails 앱을 생성하는 것부터 시작해 보겠습니다.


오늘은 창의력이 부족한 관계로, 제목과 리치 텍스트 본문을 가진 Article 모델을 스캐폴딩하겠습니다.


주의하세요! 여기에 의외의 함정이 하나 숨어 있습니다. 위 설치 명령은 CreateActionTextTables 마이그레이션을 생성하는데, 이를 CreateActionTextTablesTable로 이름을 바꿔야 합니다. 또한 null: false, default: [["", ""], ["", ""]] 옵션으로 기본값을 2x2 테이블로 설정하겠습니다.


Rails ActionText 모델에 테이블 추가하기

리치 텍스트에 실제로 테이블을 추가하기 전에, 먼저 Trix 툴바를 패치해야 합니다.


여기서는 Trix의 toolbarElement에 버튼을 직접 추가했습니다. 이 버튼을 아직 만들지 않은 trix-table 스티뮬러스(Stimulus) 컨트롤러와 연결하면 문서에 테이블이 삽입됩니다. CSS로 이 버튼에 멋진 SVG 콘텐츠를 넣고, 테이블 스타일도 함께 설정해 보겠습니다.


아래와 같이 "file-tools" 그룹에 성공적으로 추가된 것을 확인할 수 있습니다.

Rails에서 Trix와 Turbo 프레임으로 동적 테이블 편집기 만들기

이제 Turbo의 도움을 받아 테이블을 추가하고 조작하는 작업으로 돌아가겠습니다. 이를 위해 먼저 create 액션을 가진 컨트롤러가 필요합니다.


이 액션은 앞서 소개한 'On Rails' 블로그 포스트에서 거의 그대로 차용한 것으로, 클라이언트 측에서 첨부 파일을 삽입하는 데 필요한 JSON을 생성합니다. 여기에는 SGID와, 뒤에서 살펴볼 editor 파셜로 렌더링된 content가 포함됩니다.


설정에 관련된 리소스형(resourceful) 테이블 라우트를 추가합니다.


이제 깊은 곳으로 뛰어들 시간입니다. 테이블 모델을 만들어야 합니다. 우선 ActionText::Attachable을 include하고 관련 파셜 경로를 정의합니다.


아직 테이블 콘텐츠가 어떻게 저장되는지는 정의하지 않았습니다. 데이터베이스에서 JSON(B) 컬럼으로 선언했기 때문에 원하는 형식을 자유롭게 선택할 수 있습니다. 앞서 인용한 블로그 포스트에서 약간 벗어나, 여기서는 2차원 배열을 사용하겠습니다. 그러면 다음처럼 content를 중첩 루프로 간단히 순회할 수 있습니다.


위 파셜은 예를 들어 ActionView가 요청할 때마다 렌더링됩니다. 다음으로 Trix 안에서 인라인으로 사용될 editor 파셜도 만들어야 합니다.


눈치채셨겠지만 유일한 차이점은 SGID를 DOM id로 사용해 Turbo Frame으로 감싸고 있다는 점입니다. 또한 행과 열 인덱스를 구분 블록에 전달하고, 내부 DIV를 contenteditable로 만들어 인라인 편집을 준비합니다. 이 부분은 나중에 다루겠습니다.

이제 툴바의 테이블 버튼을 방금 작성한 서버 측 컨트롤러 액션에 연결해야 합니다. 이를 위해 먼저 Rails의 request.js 라이브러리를 프로젝트에 도입해야 합니다. 이 라이브러리는 적절한 CSRF 토큰 등을 포함해 클라이언트에서 post 요청을 관리하는 데 도움을 줍니다.


새로운 Trix Table 스티뮬러스 컨트롤러 만들기

모든 준비가 끝났으니 새로운 trix-table 스티뮬러스 컨트롤러를 만들어 보겠습니다. 여기에는 툴바 버튼이 참조하는 attachTable 액션을 구현합니다.


이 액션은 테이블의 create 라우트로 POST 요청을 보내고, JSON 응답을 Trix 첨부 파일로 삽입합니다. 역시 OnRails 블로그 포스트에서 가져온 코드이며, 더 이상 권장되지 않는 rails-ujs 호출을 최신 request.js 라이브러리로 교체했습니다.

이제 이 컨트롤러를 폼 마크업에 추가해서 실제로 앱에서 사용해야 합니다.


스티뮬러스의 매력은 form 요소에 데이터 속성 두 개만 추가하면 원하는 결과를 얻을 수 있다는 점입니다. 이제 버튼 한 번의 클릭으로 게시글 콘텐츠에 테이블을 추가할 수 있게 되었습니다.

Rails에서 Trix와 Turbo 프레임으로 동적 테이블 편집기 만들기

Turbo Frame으로 테이블 조작하기

이제 테이블 첨부 파일을 생성할 수 있게 되었으니, 콘텐츠 조작에 초점을 맞춰 보겠습니다. 놀랍게도 Turbo Frame은 이 용도에 거의 완벽하게 잘 맞습니다.

테이블 행과 열 추가 및 삭제

행과 열을 추가·삭제하기 위해 네 가지 연산 각각에 대응하는 버튼 네 개로 구성된 미니 툴바를 만듭니다. button_to 헬퍼를 활용하고 URL을 해당 테이블의 update 라우트로 지정합니다. 실행하려는 연산을 추가 파라미터로 함께 넘겨줍니다.


그에 따라 TablesController에도 해당 컨트롤러 액션을 추가해야 합니다. update 액션이 이 연산들을 모델에 위임한다는 점에 주목하세요.


테이블 구조 변경 사항이 저장되면, 테이블의 edit 뷰로 리디렉션합니다. 이 뷰는 동일한 editor 파셜을 렌더링하는데, 결과적으로 동일한 Turbo Frame을 참조하게 됩니다. 덕분에 Turbo가 일치하는 프레임을 감지해 서로 교체할 수 있습니다.


이제 Table 모델에 누락된 명령들을 구현해야 합니다.


주목할 점은, 단순한 2차원 배열이라는 데이터 구조 덕분에 add/remove<sub>column</sub>/row 메서드들이 열과 행의 개수만 수정하는 단순한 프록시 역할을 한다는 것입니다. 이것까지 완료하면 버튼 클릭만으로 테이블 구조를 변경할 수 있습니다.

Rails에서 Trix와 Turbo 프레임으로 동적 테이블 편집기 만들기

테이블 셀 내용 편집하기

열과 행의 개수를 변경하는 것 외에도 셀 내용을 편집하고 싶습니다. 이를 위해 역시 앞서 소개한 블로그 포스트에 크게 의존해 스티뮬러스 테이블 에디터 컨트롤러를 만들겠습니다.


updateCell 메서드는 셀이 편집될 때마다 PATCH 요청을 보내며, 행과 열 인덱스를 파라미터로 전달합니다. 이제 이를 DOM에 연결하기만 하면 됩니다.


물론 서버 측 TablesController도 이 연산을 처리할 방법이 필요합니다. 다행히 간소화된 개념 검증(proof of concept) 단계에서는 조건문에 분기 하나만 추가하면 쉽게 해결됩니다. update 액션이 JSON 타입 요청도 처리할 수 있도록 하는데, 여기서는 빈 객체만 반환하면 충분합니다.


참고로 프로덕션 앱에서는 if/elsif/else 조건문 대신 다른 방식으로 연산을 정제(sanitizing)할 것을 권장합니다. 저라면 이 경우 Mediator나 Proxy 패턴을 사용할 것 같습니다.

Ruby에서 Trix의 한계

지금까지의 설명은 완벽하게 타당해 보이지만, 중요한 세부 사항 하나를 생략했습니다. 데이터베이스 모델은 잘 저장되지만, Trix의 내부 섀도(shadow) 표현과는 동기화되지 않습니다. 그래서 테이블에서 포커스를 벗어나면 이전에 저장된 표현으로 되돌아가 버립니다.

Rails에서 Trix와 Turbo 프레임으로 동적 테이블 편집기 만들기

지금 페이지를 새로 고침하면 추가된 콘텐츠가 나타납니다. Trix의 문서가 새로 초기화되기 때문입니다.

저는 이 문제의 원인을 선택(selection)이 변경될 때 Trix가 내부 문서를 동기화하는 지점으로 좁혔습니다. 바로 그 지점에서 섀도 요소로부터 문서를 다시 펼치기 때문입니다.

turbo:submit 이벤트에 훅을 걸어 테이블에서 포커스가 벗어나는 순간 동기화를 막는 방법을 시도해 봤지만, 제가 생각해 낸 해결책들은 모두 매우 지저분하고 내부 API에 크게 의존적이었습니다.

Turbo스럽게 이 문제를 처리하는 가장 좋은 방법은, 아마도 폼 전체를 eager 로드되는 Turbo Frame으로 감싸고 Trix 콘텐츠가 변경될 때마다 리로드하도록 지시하는 것일 겁니다.

대략 다음과 같은 방식이면 해결할 수 있을 것입니다.


폼을 src에서 로드하는 Turbo Frame으로 감싼다면 말이죠.


다만 이 접근 방식은 이미 영속화된 베이스 레코드에만 동작합니다.

Trix에 대한 마지막 경고

우리가 만든 이 개념 검증은 서버 렌더링 HTML을 사용해, 테이블을 JSON으로 직렬화하고 JavaScript 이벤트를 듣는 추가적인 복잡성을 없앴습니다. 어떤 ActionText 설치 환경으로든 이식 가능하며, 쉽게 gem으로 추출할 수도 있습니다.

하지만 몇 가지 단점도 있습니다. 가장 명확한 것은 Trix의 문서 모델과 재동기화가 필요하다는 점입니다. 제안한 우회 방법이 실용적인 상황도 있고, 아예 불가능한 상황도 있을 수 있습니다. Trix가 Turbo 호환 인터페이스를 갖추기 전까지는 이를 피할 방법이 없습니다.

두 번째 문제는 Trix의 undo(되돌리기) 기능을 사용하지 않는다는 점입니다(하지만 이는 모든 Trix 첨부 파일에 해당하는 사항입니다). 마찬가지로 내부 API를 수정하기보다 업스트림 변경을 기다리는 것이 현명합니다.

마무리

이번 포스트에서는 ActionText 첨부 파일의 기본 개념을 간단히 살펴보는 것으로 시작했습니다. 이후 ActionText 모델에 테이블을 추가하고, Turbo Frame으로 이를 조작했습니다. 마지막으로 Trix 사용의 몇 가지 한계도 짚어 보았습니다.

CoffeeScript를 현대적인 순수 JavaScript로 변환하는 작업이 진행 중인 Trix v2가 출시를 앞두고 있는 만큼, 지금이 Turbo 호환성을 다룰 좋은 시기입니다. 물론 그런 래퍼(wrapper)가 어떤 모습일지 구상하는 것은 현재 제 능력 범위를 넘어서지만, 분명 기회의 창이 열려 있는 것 같습니다.

즐거운 코딩 되세요!

P.S. Ruby Magic 포스트가 발행되는 대로 읽고 싶으시다면 Ruby Magic 뉴스레터를 구독하고 어떤 글도 놓치지 마세요!