Rails에서 동적 OG 이미지
출처: Dev.to
Rails 애플리케이션의 모든 블로그 포스트, 제품 페이지, 프로필은 자체 Open Graph 이미지가 필요합니다. 이 이미지는 트위터, LinkedIn, Slack 등에서 링크를 공유했을 때 표시되는 그림입니다. 문제는 Ruby에서 이러한 이미지를 생성할 수 있는 옵션이 JavaScript와 PHP 세계에 비해 열악하다는 점입니다. Satori도 없고, 이미지‑템플릿화에 대한 일류 지원도 없으며, 존재하는 도구들은 직접 픽셀을 배치하거나 Puma 옆에 헤드리스 브라우저를 띄워야 합니다.
이 글에서는 Rails에서 현실적인 세 가지 방법을 살펴본 뒤, 뷰 레이어와 서버를 깔끔하게 유지할 수 있는 방법—HTTP를 통해 ERB 템플릿을 PNG로 렌더링하고, 캐시와 백그라운드 잡을 사용해 요청 경로에 부담을 주지 않는 방법—을 구현합니다.
※ 이 글은 처음에 html2img.com에 게재된 글을 교차 게시한 것입니다.
접근 방식
| 방법 | 실행 환경 | 가장 적합한 경우 |
|---|---|---|
| 이미지 라이브러리 (MiniMagick, ruby‑vips) | 서버에 ImageMagick 또는 libvips 설치 | 텍스트가 거의 없고 레이아웃이 고정된 경우 |
| 헤드리스 Chrome (Grover) | Puma와 함께 Node와 Chromium 바이너리 배치 | 전체 CSS 제어가 필요하고 운영 비용을 감당할 수 있는 경우 |
| HTML → Image API | 단일 HTTP 호출, 로컬에 별도 설치 필요 없음 | 브라우저를 실행하지 않고도 CSS 레이아웃을 사용하고 싶은 경우 |
순수 Ruby 방식: MiniMagick / ruby‑vips
기본 이미지를 열고 텍스트를 합성합니다.
require "mini_magick"
image = MiniMagick::Image.open(Rails.root.join("app/assets/images/og-base.png"))
image.combine_options do |c|
c.gravity "NorthWest"
c.pointsize "64"
c.fill "#0F172A"
c.font "Inter-Bold"
c.annotate "+80+80", post.title
end
image.write(Rails.root.join("public/og/#{post.id}.png"))
텍스트가 복잡해지면 문제가 생깁니다. 모든 요소를 손수 배치해야 하고, 줄 바꿈도 없으며 문자열이 차지할 너비를 측정하기 전까지는 알 수 없습니다. 또한 모든 머신에 폰트 파일을 설치하고 참조해야 합니다. 고정 배경에 짧은 한 줄 정도라면 괜찮지만, 줄 바꿈이 필요한 제목, 저자 라인, 로고 등을 넣으려면 ImageMagick 옵션으로 CSS 레이아웃을 재구현하는 셈이 됩니다. 도구의 역할에 맞지 않는 접근입니다.
실제 브라우저 사용: Grover
CSS가 필요하다면 실제 브라우저에서 HTML을 렌더링하는 것이 직관적입니다. Grover는 Puppeteer를 래핑해 HTML을 PNG로 변환합니다.
# Gemfile
gem "grover"
html = ApplicationController.render(
template: "og/post",
layout: false,
assigns: { post: post }
)
png = Grover.new(html, width: 1200, height: 630).to_png
File.binwrite(Rails.root.join("public/og/#{post.id}.png"), png)
출력 품질은 브라우저 덕분에 뛰어나지만, 운영 비용이 발생합니다. Grover는 Puppeteer를 구동하므로 이미지 생성 머신마다 Node와 Chromium 바이너리를 설치해야 합니다. 컨테이너 환경이라면 수백 메가바이트 규모의 Chrome 이미지가 필요하고, 프로세스가 시작될 때 콜드 스타트 지연도 발생합니다. 즉, 사진을 만들기 위해 브라우저를 운영하게 되는 것입니다.
API 활용: HTML → Image
세 번째 옵션은 브라우저 수준의 렌더링 품질을 유지하면서 인프라에서 분리하는 방법입니다. OG 카드를 일반 ERB 템플릿으로 작성하고, 이를 HTML 문자열로 변환한 뒤 이미지 API에 POST합니다. 한 번의 HTTP 호출만 있으면 되며, 별도 설치가 필요 없습니다.
# rails credentials에 추가 (rails credentials:edit)
html2img:
api_key: your-key-here
키는 모든 요청에 X-API-Key 헤더로 전송됩니다. 이제 템플릿을 만듭니다. 인라인 CSS로 스타일링하고, API에 요청할 정확한 크기로 맞춥니다.
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap');
* { margin: 0; box-sizing: border-box; }
body {
width: 1200px; height: 630px; padding: 80px;
display: flex; flex-direction: column; justify-content: space-between;
background: #0F172A; color: #F8FAFC; font-family: 'Inter', sans-serif;
}
.title { font-size: 64px; font-weight: 800; line-height: 1.1; max-width: 1000px; }
.meta { font-size: 26px; color: #94A3B8; }
</style>
<div class="title"><%= post.title %></div>
<div class="meta"><%= post.author %> · <%= post.published_at.to_s(:short) %></div>
body에 지정한 너비와 높이가 API에 전달할 크기와 일치해야 합니다. 실제 브라우저에서 렌더링되므로 이모지, 웹 폰트, 전체 CSS가 그대로 동작합니다.
서비스 객체로 감싸기
ApplicationController.render는 템플릿을 문자열로 변환하고, Faraday를 이용해 API에 POST합니다.
# app/services/og_image_generator.rb
class OgImageGenerator
ENDPOINT = "https://app.html2img.com/api/html".freeze
class GenerationError < StandardError; end
def initialize(post)
@post = post
end
def call
html = ApplicationController.render(
template: "og/post",
layout: false,
assigns: { post: @post }
)
response = Faraday.post(
ENDPOINT,
{ html: html }.to_json,
{
"Content-Type" => "application/json",
"X-API-Key" => Rails.application.credentials.dig(:html2img, :api_key)
}
)
raise GenerationError, response.body unless response.success?
result = JSON.parse(response.body)
@post.update!(og_image_url: result["url"])
end
end
메타 태그 설정
twitter:card를 summary_large_image 로 지정하면 썸네일이 아니라 전체 폭 이미지가 표시됩니다.
테스트에서 HTTP 호출 스텁하기
# test/jobs/generate_og_image_job_test.rb
require "test_helper"
class GenerateOgImageJobTest < ActiveJob::TestCase
test "generates OG image and saves URL" do
post = posts(:one)
stub_request(:post, OgImageGenerator::ENDPOINT)
.to_return(
status: 200,
body: { url: "https://i.html2img.com/abc123.png" }.to_json,
headers: { "Content-Type" => "application/json" }
)
GenerateOgImageJob.perform_now(post)
assert_equal "https://i.html2img.com/abc123.png", post.reload.og_image_url
end
end
위 코드는 대부분의 Rails 테스트 환경에 기본으로 포함된 WebMock을 사용합니다. RSpec에서도 동일한 패턴을 적용할 수 있습니다.
언제 어떤 방법을 써야 할까?
- 레이아웃이 고정되고 텍스트가 거의 없을 때는 MiniMagick을 사용합니다.
- 자체 인프라에서 브라우저가 반드시 필요하고 OG 이미지가 부수적인 혜택일 경우 Grover를 선택합니다.
- 그 외 대부분의 경우는 HTML → Image API를 이용해 ERB 템플릿을 렌더링하고, 캐시와 백그라운드 잡을 결합하면 서버에 Chrome을 두지 않아도 되고, 실제 CSS를 그대로 사용할 수 있으며, 이미지당 한 번의 HTTP 호출 비용만 발생합니다.
전체 글과 추가적인 팁은 html2img.com에서 확인할 수 있습니다. 현재 여러분은 Rails 앱에서 OG 이미지를 어떻게 생성하고 있나요? 댓글로 알려 주세요.