1 Action Text란?
Action Text는 rich text 콘텐츠의 처리와 표시를 용이하게 합니다. rich text 콘텐츠는 굵게, 기울임꼴, 색상, 하이퍼링크와 같은 서식 요소를 포함하는 텍스트로, 일반 텍스트를 넘어서는 시각적으로 향상되고 구조화된 표현을 제공합니다. 이를 통해 rich text 콘텐츠를 생성하고 테이블에 저장한 다음, 모든 model에 첨부할 수 있습니다.
Action Text는 Trix라는 WYSIWYG 편집기를 포함하며, 이는 웹 애플리케이션에서 사용자에게 rich text 콘텐츠를 생성하고 편집할 수 있는 사용자 친화적인 인터페이스를 제공합니다. 텍스트 서식 지정, 링크나 인용구 추가, 이미지 삽입 등과 같은 풍부한 기능을 모두 처리합니다. 예시는 the Trix editor website를 참조하세요.
Trix 편집기로 생성된 rich text 콘텐츠는 자체 RichText model에 저장되며, 이는 애플리케이션의 모든 기존 Active Record model과 연결될 수 있습니다. 또한, 삽입된 이미지(또는 기타 첨부 파일)는 Active Storage(의존성으로 추가됨)를 사용하여 자동으로 저장되고 해당 RichText model과 연결될 수 있습니다. 콘텐츠를 렌더링할 때, Action Text는 페이지의 HTML에 직접 삽입해도 안전하도록 콘텐츠를 먼저 sanitizing하여 처리합니다.
대부분의 WYSIWYG 편집기는 HTML의 contenteditable
과 execCommand
API를 감싸고 있습니다. 이러한 API들은 Internet Explorer 5.5에서 웹 페이지의 실시간 편집을 지원하기 위해 Microsoft가 설계했습니다. 이후 다른 브라우저들이 이를 리버스 엔지니어링하고 복사했습니다. 결과적으로 이러한 API들은 완전히 명세화되거나 문서화되지 않았고, WYSIWYG HTML 편집기가 매우 방대한 범위를 다루기 때문에 각 브라우저의 구현은 자체적인 버그와 특이사항을 가지고 있습니다. 따라서 JavaScript 개발자들은 종종 이러한 불일치를 해결해야 합니다.
Trix는 contenteditable
을 I/O 장치로 취급함으로써 이러한 불일치를 피해갑니다: 입력이 편집기에 도달하면, Trix는 해당 입력을 내부 문서 모델에 대한 편집 작업으로 변환합니다.
그런 다음 해당 document를 editor로 다시 렌더링합니다. 이를 통해 Trix는 모든 keystroke 이후에 발생하는 일을 완벽하게 제어할 수 있으며, execCommand
와 그에 따른 불일치를 사용할 필요가 없습니다.
2 설치
Action Text를 설치하고 rich text 콘텐츠 작업을 시작하려면 다음을 실행하세요:
$ bin/rails action_text:install
다음 작업들이 수행됩니다:
trix
와@rails/actiontext
를 위한 JavaScript 패키지들을 설치하고 이를application.js
에 추가합니다.- 이미지와 기타 첨부 파일을 Active Storage로 분석하고 변환하기 위한
image_processing
gem을 추가합니다. 이에 대한 자세한 정보는 Active Storage Overview 가이드를 참조하세요. - rich text 콘텐츠와 첨부 파일을 저장하는 다음 테이블들을 생성하기 위한 migration을 추가합니다:
action_text_rich_texts
,active_storage_blobs
,active_storage_attachments
,active_storage_variant_records
. - 모든 Trix 스타일과 오버라이드를 포함하는
actiontext.css
를 생성합니다. - Action Text 콘텐츠와 Active Storage 첨부 파일(blob)을 각각 렌더링하기 위한 기본 view 파셜인
_content.html
과_blob.html
을 추가합니다.
이후 migration을 실행하면 새로운 action_text_*
와 active_storage_*
테이블들이 앱에 추가됩니다:
$ bin/rails db:migrate
Action Text 설치가 action_text_rich_texts
테이블을 생성할 때, 여러 모델이 rich text 속성을 추가할 수 있도록 polymorphic 관계를 사용합니다. 이는 모델의 ClassName과 레코드의 ID를 각각 저장하는 record_type
과 record_id
컬럼을 통해 이루어집니다.
polymorphic 관계를 통해 모델은 단일 관계에서 둘 이상의 다른 모델에 속할 수 있습니다. 이에 대해 자세히 알아보려면 Active Record Associations 가이드를 참조하세요.
따라서, Action Text 콘텐츠를 포함하는 모델이 식별자로 UUID 값을 사용하는 경우, Action Text 속성을 사용하는 모든 모델은 고유 식별자로 UUID 값을 사용해야 합니다. Action Text를 위해 생성된 migration도 record references 라인에 type: :uuid
를 지정하도록 업데이트되어야 합니다.
t.references :record, null: false, polymorphic: true, index: false, type: :uuid
3 Rich Text 콘텐츠 생성하기
이 섹션에서는 rich text를 생성하기 위해 필요한 설정들을 살펴봅니다.
RichText 레코드는 Trix 에디터에서 생성된 콘텐츠를 serialized된 body
속성에 저장합니다. 또한 Active Storage를 사용하여 저장된 임베디드 파일들에 대한 모든 참조를 보관합니다. 이 레코드는 rich text 콘텐츠를 가지려는 Active Record 모델과 연결됩니다. 연결은 rich text를 추가하고자 하는 모델에 has_rich_text
클래스 메서드를 배치하여 이루어집니다.
# app/models/article.rb
class Article < ApplicationRecord
has_rich_text :content
end
Article 테이블에 content
컬럼을 추가할 필요가 없습니다. has_rich_text
는 생성된 action_text_rich_texts
테이블과 콘텐츠를 연결하고 이를 모델과 다시 연결합니다. content
외의 다른 이름으로 속성의 이름을 지정할 수도 있습니다.
모델에 has_rich_text
클래스 메서드를 추가한 후에는 해당 필드에 대한 rich text 에디터(Trix)를 사용하도록 뷰를 업데이트할 수 있습니다. 이를 위해 폼 필드에 rich_textarea
를 사용하세요.
<%# app/views/articles/_form.html.erb %>
<%= form_with model: article do |form| %>
<div class="field">
<%= form.label :content %>
<%= form.rich_textarea :content %>
</div>
<% end %>
이것은 rich text를 적절하게 생성하고 업데이트하는 기능을 제공하는 Trix 에디터를 표시합니다. 나중에 에디터의 스타일을 업데이트하는 방법에 대해 자세히 살펴보겠습니다.
마지막으로, 에디터에서의 업데이트를 수락할 수 있도록 관련 컨트롤러에서 참조된 속성을 파라미터로 허용해야 합니다:
class ArticlesController < ApplicationController
def create
article = Article.create! params.expect(article: [:title, :content])
redirect_to article
end
end
has_rich_text
를 사용하는 클래스의 이름을 변경해야 하는 경우, action_text_rich_texts
테이블의 해당 행에 있는 polymorphic type 컬럼인 record_type
도 함께 업데이트해야 합니다.
Action Text는 polymorphic associations에 의존하고 있으며, 이는 데이터베이스에 클래스 이름을 저장하는 것과 관련이 있기 때문에 Ruby 코드에서 사용되는 클래스 이름과 데이터를 동기화하는 것이 중요합니다. 이러한 동기화는 저장된 데이터와 코드베이스의 클래스 참조 간의 일관성을 유지하는 데 필수적입니다.
4 Rich Text 콘텐츠 렌더링
ActionText::RichText
인스턴스는 이미 안전한 렌더링을 위해 콘텐츠가 살균(sanitize)되어 있기 때문에 페이지에 직접 임베드할 수 있습니다. 다음과 같이 콘텐츠를 표시할 수 있습니다:
<%= @article.content %>
ActionText::RichText#to_s
는 RichText를 HTML String으로 안전하게 변환합니다. 반면에 ActionText::RichText#to_plain_text
는 HTML 안전하지 않은 문자열을 반환하므로 브라우저에서 렌더링해서는 안 됩니다. Action Text의 살균 프로세스에 대해서는 ActionText::RichText
문서에서 자세히 알아볼 수 있습니다.
content
필드 내에 첨부된 리소스가 있는 경우, Active Storage의 필수 요소를 설치하지 않으면 제대로 표시되지 않을 수 있습니다.
5 Rich Text Content Editor (Trix) 커스터마이징
에디터의 외관을 당신의 스타일 요구사항에 맞게 업데이트하고 싶을 때가 있을 것입니다. 이 섹션에서는 그 방법을 안내합니다.
5.1 Trix 스타일 제거 또는 추가
기본적으로 Action Text는 .trix-content
class가 있는 element 내부에 rich text content를 렌더링합니다. 이는 app/views/layouts/action_text/contents/_content.html.erb
에 설정되어 있습니다. 이 class를 가진 element들은 trix stylesheet에 의해 스타일이 적용됩니다.
trix 스타일을 수정하고 싶다면 app/assets/stylesheets/actiontext.css
에 custom 스타일을 추가할 수 있습니다. 이 파일에는 Trix의 전체 스타일 세트와 Action Text에 필요한 override가 포함되어 있습니다.
5.2 Editor Container 커스터마이징하기
rich text 콘텐츠 주변에 렌더링되는 HTML container 요소를 커스터마이징하려면, installer가 생성한 app/views/layouts/action_text/contents/_content.html.erb
레이아웃 파일을 편집하세요:
<%# app/views/layouts/action_text/contents/_content.html.erb %>
<div class="trix-content">
<%= yield %>
</div>
5.3 삽입된 이미지와 첨부 파일을 위한 HTML 커스터마이징
삽입된 이미지와 기타 첨부 파일(blob이라고 알려진)을 위해 렌더링되는 HTML을 커스터마이징하려면, installer가 생성한 app/views/active_storage/blobs/_blob.html.erb
템플릿을 수정하세요:
<%# app/views/active_storage/blobs/_blob.html.erb %>
<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
<% if blob.representable? %>
<%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
<% end %>
<figcaption class="attachment__caption">
<% if caption = blob.try(:caption) %>
<%= caption %>
<% else %>
<span class="attachment__name"><%= blob.filename %></span>
<span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
<% end %>
</figcaption>
</figure>
6 첨부 파일
현재 Action Text는 Active Storage를 통해 업로드된 첨부 파일과 Signed GlobalID에 연결된 첨부 파일을 지원합니다.
6.1 Active Storage
리치 텍스트 에디터 내에서 이미지를 업로드할 때는 Action Text를 사용하며, 이는 다시 Active Storage를 사용합니다. 하지만, Active Storage에는 몇 가지 의존성이 있으며 이는 Rails에서 제공하지 않습니다. 내장된 프리뷰어를 사용하려면 이러한 라이브러리들을 설치해야 합니다.
에디터에서 예상되는 업로드의 종류에 따라 이러한 라이브러리들 중 일부만 필요하며 모두가 필요한 것은 아닙니다. Action Text와 Active Storage를 사용할 때 사용자들이 자주 겪는 일반적인 오류는 에디터에서 이미지가 제대로 렌더링되지 않는다는 것입니다. 이는 보통 libvips
의존성이 설치되지 않았기 때문입니다.
6.1.1 Attachment Direct Upload JavaScript Events
이벤트 이름 | 이벤트 대상 | 이벤트 데이터 (event.detail ) |
설명 |
---|---|---|---|
direct-upload:start |
<input> |
{id, file} |
Direct upload가 시작됨 |
direct-upload:progress |
<input> |
{id, file, progress} |
파일 저장 요청이 진행되는 동안 |
direct-upload:error |
<input> |
{id, file, error} |
오류가 발생함. 이 이벤트가 취소되지 않으면 alert 가 표시됨 |
direct-upload:end |
<input> |
{id, file} |
Direct upload가 종료됨 |
6.2 Signed GlobalID
Action Text는 Active Storage로 업로드되는 첨부 파일 외에도 Signed GlobalID로 해석될 수 있는 모든 것을 임베드할 수 있습니다.
Global ID는 모델 인스턴스를 고유하게 식별하는 앱 전체의 URI입니다: gid://YourApp/Some::Model/id
. 이는 서로 다른 클래스의 객체를 참조하기 위한 단일 식별자가 필요할 때 유용합니다.
이 방법을 사용할 때, Action Text는 첨부 파일에 signed global ID(sgid)가 필요합니다. 기본적으로 Rails 앱의 모든 Active Record 모델은 GlobalID::Identification
concern을 믹스인하므로, signed global ID로 해석될 수 있고 따라서 ActionText::Attachable
와 호환됩니다.
Action Text는 나중에 최신 콘텐츠로 다시 렌더링할 수 있도록 저장 시 삽입한 HTML을 참조합니다. 이를 통해 모델을 참조할 수 있고 해당 레코드가 변경될 때 항상 최신 콘텐츠를 표시할 수 있습니다.
Action Text는 global ID에서 모델을 로드한 다음 콘텐츠를 렌더링할 때 기본 partial 경로로 렌더링합니다.
Action Text 첨부 파일은 다음과 같이 보일 수 있습니다:
<action-text-attachment sgid="BAh7CEkiCG…"></action-text-attachment>
Action Text는 요소의 sgid 속성을 인스턴스로 해석하여 임베드된 <action-text-attachment>
요소를 렌더링합니다. 해석이 완료되면, 해당 인스턴스는 render 헬퍼에 전달됩니다. 그 결과, HTML은 <action-text-attachment>
요소의 하위 요소로 임베드됩니다.
Action Text <action-text-attachment>
요소 내에서 첨부 파일로 렌더링되기 위해서는 GlobalID::Identification
concern을 통해 제공되는 #to_sgid(**options)
를 구현하는 ActionText::Attachable
모듈을 포함해야 합니다.
또한 선택적으로 커스텀 partial 경로를 렌더링하기 위한 #to_attachable_partial_path
와 누락된 레코드를 처리하기 위한 #to_missing_attachable_partial_path
를 선언할 수 있습니다.
예시는 다음과 같습니다:
class Person < ApplicationRecord
include ActionText::Attachable
end
person = Person.create! name: "Javan"
html = %Q(<action-text-attachment sgid="#{person.attachable_sgid}"></action-text-attachment>)
content = ActionText::Content.new(html)
content.attachables # => [person]
6.3 Action Text Attachment 렌더링
<action-text-attachment>
가 렌더링되는 기본적인 방법은 기본 경로 partial을 통해서입니다.
이를 더 자세히 설명하기 위해, User 모델을 살펴보겠습니다:
# app/models/user.rb
class User < ApplicationRecord
has_one_attached :avatar
end
user = User.find(1)
user.to_global_id.to_s #=> gid://MyRailsApp/User/1
user.to_signed_global_id.to_s #=> BAh7CEkiCG…
.find(id)
클래스 메서드가 있는 어떤 모델에도 GlobalID::Identification
을 믹스인할 수 있습니다. Active Record에서는 자동으로 지원이 포함되어 있습니다.
위의 코드는 모델 인스턴스를 고유하게 식별하는 식별자를 반환합니다.
다음으로, User 인스턴스의 signed GlobalID를 참조하는 <action-text-attachment>
요소가 포함된 리치 텍스트 콘텐츠를 살펴보겠습니다:
<p>Hello, <action-text-attachment sgid="BAh7CEkiCG…"></action-text-attachment>.</p>
Action Text는 "BAh7CEkiCG…" 문자열을 사용하여 User 인스턴스를 찾습니다. 그런 다음 콘텐츠를 렌더링할 때 기본 partial 경로로 렌더링합니다.
이 경우, 기본 partial 경로는 users/user
partial입니다:
<%# app/views/users/_user.html.erb %>
<span><%= image_tag user.avatar %> <%= user.name %></span>
따라서, Action Text에 의해 렌더링되는 최종 HTML은 다음과 같습니다:
<p>Hello, <action-text-attachment sgid="BAh7CEkiCG…"><span><img src="..."> Jane Doe</span></action-text-attachment>.</p>
6.4 action-text-attachment를 위한 다른 Partial 렌더링하기
attachable에 대해 다른 partial을 렌더링하려면, User#to_attachable_partial_path
를 정의하세요:
class User < ApplicationRecord
def to_attachable_partial_path
"users/attachable"
end
end
그런 다음 해당 partial을 선언하세요. User 인스턴스는 user라는 partial-local 변수로 사용할 수 있습니다:
<%# app/views/users/_attachable.html.erb %>
<span><%= image_tag user.avatar %> <%= user.name %></span>
6.5 Action Text가 Instance를 해석할 수 없거나 action-text-attachment가 누락된 경우 Partial 렌더링하기
Action Text가 User instance를 해석할 수 없는 경우(예: 레코드가 삭제된 경우), 기본 fallback partial이 렌더링됩니다.
다른 누락된 attachment partial을 렌더링하려면, 클래스 레벨의 to_missing_attachable_partial_path
메서드를 정의하세요:
class User < ApplicationRecord
def self.to_missing_attachable_partial_path
"users/missing_attachable"
end
end
그리고 해당 partial을 선언하세요.
<%# app/views/users/missing_attachable.html.erb %>
<span>Deleted user</span>
6.6 API를 통한 첨부
당신의 아키텍처가 전통적인 Rails 서버 사이드 렌더링 패턴을 따르지 않는다면, 파일 업로드를 위한 별도의 엔드포인트가 필요한 백엔드 API(예: JSON 사용)를 가지고 있을 수 있습니다. 이 엔드포인트는 ActiveStorage::Blob
을 생성하고 attachable_sgid
를 반환해야 합니다:
{
"attachable_sgid": "BAh7CEkiCG…"
}
그 후, attachable_sgid
를 가져와서 프론트엔드 코드에서 <action-text-attachment>
태그를 사용하여 리치 텍스트 컨텐츠에 삽입할 수 있습니다:
<action-text-attachment sgid="BAh7CEkiCG…"></action-text-attachment>
7 기타
7.1 N+1 쿼리 피하기
만약 종속된 ActionText::RichText
모델을 preload하고 싶다면, rich text 필드의 이름이 content
라고 가정했을 때 다음과 같은 named scope를 사용할 수 있습니다:
Article.all.with_rich_text_content # attachment 없이 본문을 preload합니다.
Article.all.with_rich_text_content_and_embeds # 본문과 attachment 모두를 preload합니다.