Rails 8 + Hotwire Native 앱의 역할 기반 UI 분리와 모바일 최적화 삽질기

Rails 8 + Hotwire Native 조합으로 iOS 앱을 운영하는 중에, 하루 동안 발생한 여러 문제를 연쇄적으로 해결한 기록이다. 작은 UI 깨짐에서 시작해서 권한 체계 재설계까지 이어진 과정을 정리한다. Hotwire Native의 핵심 매력은 하나의 Rails 앱으로 웹과 네이티브 iOS/Android를 동시에 지원한다는 점이다. 하지만 이 구조는 “웹에서 잘 보이면 앱에서도 잘 보인다"는 착각을 쉽게 심어준다. 실제로는 WKWebView의 렌더링 환경, 네이티브 네비게이션 바의 존재, 역할별 UI 분기 등 웹 브라우저와 전혀 다른 고려사항이 따라온다. ...

2026-03-17 · 7분 소요 · Seunghan
Tailwind v4 CSS Variable Theme System

Tailwind v4 CSS 변수 오버라이드로 앱 전체 테마 교체하기

테마 시스템을 구현할 때 흔히 생각하는 방법은 컴포넌트마다 조건부 클래스를 추가하는 것이다. 하지만 기존 코드를 건드리지 않고 CSS 변수 한 블록만으로 앱 전체 색상을 바꿀 수 있다면? Tailwind v4에서는 그게 가능하다. Tailwind v4의 CSS 변수 컴파일 방식 이 패턴 전체를 가능하게 만드는 아키텍처 변화는 미묘하지만 근본적이다. Tailwind v4는 유틸리티 클래스를 하드코딩된 값이 아니라 CSS 변수 참조로 컴파일한다. /* Tailwind v4가 생성하는 CSS */ .bg-emerald-700 { background-color: var(--color-emerald-700); } .text-emerald-600 { color: var(--color-emerald-600); } .border-emerald-500 { border-color: var(--color-emerald-500); } Tailwind v3에서는 bg-emerald-700이 background-color: #047857로 컴파일됐다. 스타일시트에 hex 값이 직접 박혔다. v4에서는 background-color: var(--color-emerald-700)으로 컴파일된다. 실제 색상 값은 :root에 선언된 CSS 커스텀 프로퍼티에 저장된다. ...

2026-03-17 · 7분 소요 · Seunghan

Rails 깜짝 과제 기능 + 1회성 알림 배너 — 기존 모델 재활용과 localStorage 활용

스터디를 운영하다 보면 세션 중간에 즉석으로 과제를 내야 할 때가 있다. 기존 관리자 페이지를 통하면 여러 단계를 거쳐야 하고, 멘티들은 새 과제가 생긴 걸 바로 알 수 없다는 문제가 있었다. 이 글에서는 새 모델 없이 기존 시스템을 재활용하여 깜짝 과제 기능을 만들고, 1회성 알림 배너로 멘티에게 즉시 알려주는 구현 과정을 정리한다. 문제 정의 과제 생성이 느리다: 관리자 대시보드에서 여러 필드를 채워야 한다 멘티가 모른다: 새 과제가 생겨도 목록을 직접 확인하기 전까지 알 수 없다 1회성이어야 한다: 알림을 본 뒤에는 다시 보여주지 않아야 한다 설계 결정: 새 모델 vs 기존 모델 재활용 처음에는 QuickAssignment나 Notification 같은 새 모델을 만들 수 있었지만, 분석해보니 기존 구조로 충분했다. ...

2026-03-12 · 5분 소요 · Seunghan
Slack Events API Auto Collection Rails

Slack Events API로 채널 메시지 자동 수집하기 — Rails 서비스 설계

Slack 봇에 @봇 이관이라고 멘션해야만 메시지가 수집되는 구조였다. 멘토가 매번 봇을 호출하는 게 번거롭다는 피드백이 왔다. “채널에 글이 올라오면 알아서 수집하면 안 되냐?“는 질문에서 시작된 작업 기록이다. 기존 구조: app_mention 기반 기존에는 Slack의 app_mention 이벤트만 구독하고 있었다. def handle_event(event) case event["type"] when "app_mention" handle_mention(event) end end 누군가 @봇 이관 또는 @봇 피드백 홍길동 잘했어요라고 멘션하면 처리되는 구조. 문제는: 멘토가 매번 봇을 불러야 한다 — 피드백을 쓰고 나서 다시 봇을 호출하는 이중 작업 수강생 제출물도 수동 수집 — 과제 채널에 올라온 메시지를 누군가 이관해줘야 함 파일만 올린 경우 놓침 — 텍스트 없이 파일만 공유하면 수집되지 않음 해결: 세 가지 이벤트 추가 구독 Slack 앱 설정에서 Bot Events에 다음을 추가했다: ...

2026-03-12 · 4분 소요 · Seunghan
Slack File To Activestorage Rails Seeds

Slack 파일을 Rails 프로젝트에 반영하기 — URL 저장이 아닌 소스 포함

팀원들이 Slack 채널에 HTML 파일을 과제로 제출하고 있었다. Rails 앱의 제출 상세 페이지에서 이 파일들을 인라인으로 미리보기할 수 있게 만들어야 했다. “URL 저장하면 되겠지"라는 생각으로 시작했다가 세 번의 방향 전환을 거쳤다. 1차 시도: Slack 파일 URL을 그대로 seeds에 저장 Slack의 파일 공유 URL은 이런 형태다: https://slack-files.com/T0xxx-F0xxx-hash 이걸 seeds.rb에 넣고 SlackFileImporter로 다운로드하면 ActiveStorage에 자동 첨부되는 구조가 이미 있었다. SlackFileImporter.new(submission, slack_url).call 문제: SlackFileImporter는 내부적으로 SLACK_BOT_TOKEN 환경변수를 사용한다. 배포 환경에는 토큰이 있지만, seeds가 실행되는 시점에 Slack API 호출이 실패하면 파일이 누락된다. 그리고 근본적으로 slack-files.com URL은 인증 없이 외부 웹에서 접근이 안 된다. ...

2026-03-12 · 4분 소요 · Seunghan

Telegram 봇 Inline Keyboard 버튼이 무반응인 버그 — UUID Regex + Solid Cache 디버깅

Telegram 봇에서 자연어 입력 → AI 분석 → Inline Keyboard 확인 버튼 방식으로 할 일을 추가하는 기능을 운영하던 중, 버튼을 눌러도 아무 반응이 없는 증상이 발생했다. 증상 사용자가 자연어로 일정을 입력하면 봇이 다음처럼 확인 메시지를 보낸다. 📝 할 일을 추가할까요? (개인일정) "부장님 점심식사" [📅03/24 ⏰12:00] [✅ 추가] [❌ 취소] 그런데 [✅ 추가] 버튼을 눌러도 응답이 없었다. Telegram 클라이언트에는 “알 수 없는 요청입니다.” 라는 토스트 메시지만 표시됐다. 서버 로그 확인 서버 쪽 webhook 로그를 보면 버튼 클릭은 정상적으로 서버에 도달하고 있었다. ...

2026-03-11 · 3분 소요 · Seunghan

Lookbook UX Flow 가독성 개선 — Mermaid 순서도 + Step 템플릿 리디자인

Rails + Lookbook으로 UX Flow를 문서화하다가 “이게 뭔가…” 싶은 순간이 왔다. 각 Step이 와이어프레임 조각으로만 나오니, Lookbook 목록에서 봤을 때 전체 흐름이 전혀 안 보이는 것이다. 두 가지를 고쳤다. 각 Flow에 Mermaid 순서도 Overview Step 추가 모든 Step 템플릿 구조 리디자인 문제: Lookbook Step 프리뷰가 “맥락 없는 조각"처럼 보임 # @label Admin UX Flow # @logical_path ux_flows class UxFlows::AdminFlowPreview < ViewComponent::Preview # @label 1. Login -> Admin Dashboard def step_1_login_dashboard render_with_template end # ... end 각 step_* 메서드는 render_with_template으로 ERB 파일을 렌더링한다. ERB 파일 안에는 와이어프레임이 있고, 상단에 간단한 Step 네비게이션 바가 있다. ...

2026-03-10 · 5분 소요 · Seunghan

배포는 됐는데 앱이 죽는다 — Solid Queue가 Puma를 끌고 내려간 이야기

Render에 Rails 앱을 새로 배포했다. 빌드는 성공했고 “Deploy live” 메시지도 떴다. 그런데 몇 분 뒤 대시보드에 이런 메시지가 반복됐다. Instance failed: wcvg7 Application exited early while running your code. 증상 파악 Render 로그를 뒤지니 이런 흐름이 보였다. SolidQueue::Configuration#ensure_configured_processes ← 여기서 에러 → exit 1 → "Detected Solid Queue has gone away, stopping Puma..." → Puma 종료 → 인스턴스 실패 Puma가 죽은 게 아니었다. Solid Queue가 먼저 죽고, Puma가 그걸 감지해서 스스로 내려간 것이었다. ...

2026-03-10 · 3분 소요 · Seunghan

배달앱 수수료 구조의 맹점과 Rails 8 비동기 결제 플로우 설계

배달앱 수수료 문제를 파고들다가 결제 구조의 맹점을 발견했고, 이를 우회하는 방식으로 Rails 8 아키텍처를 설계한 기록이다. 문제 인식: 카드 수수료를 낮춰줬는데 왜 체감이 없나 정부가 영세 가맹점 카드 수수료를 인하해도 배달 매출 비중이 높은 자영업자는 혜택이 거의 없다. 이유는 결제 주체가 다르기 때문이다. 결제 경로 적용 법률 영세가맹점 수수료 매장 직접 카드 결제 여신전문금융업법 0.5 ~ 0.8% 배달앱 간편결제 전자금융거래법 3.0 ~ 3.3% 배달앱을 통한 결제에서 카드사 가맹점은 **자영업자가 아니라 배달앱(또는 PG사)**이다. 자영업자는 배달앱의 “입점업체"일 뿐, 카드사와 직접 계약 관계가 없다. ...

2026-03-09 · 4분 소요 · Seunghan

Rails 앱을 Hotwire Native로 iOS 앱 만들어 TestFlight 올리기까지의 삽질 기록

Rails 8로 만든 긴급 신고 웹앱 바로신고를 Hotwire Native으로 iOS 앱으로 감싸서 TestFlight에 올리기까지의 과정을 정리합니다. 기술 스택 Backend: Rails 8 + Turbo iOS: Hotwire Native 1.2.2 + XcodeGen 빌드: Makefile 자동화 프로젝트 구조 ios/ ├── project.yml # XcodeGen 설정 ├── ExportOptions.plist # App Store 내보내기 ├── Makefile # 빌드 자동화 └── BaroSingo/ ├── AppDelegate.swift ├── SceneController.swift ├── AppTab.swift ├── Bridge/ │ ├── FormComponent.swift │ ├── HapticComponent.swift │ └── ShareComponent.swift └── Resources/ ├── Assets.xcassets/ └── path-configuration.json 삽질 1: Hotwire Native API 변경 Hotwire.config.userAgent — 읽기 전용 // ❌ 컴파일 에러: 'userAgent' is a get-only property Hotwire.config.userAgent = "BaroSingo iOS" // ✅ 해결: makeCustomWebView 사용 Hotwire.config.makeCustomWebView = { configuration in let webView = WKWebView(frame: .zero, configuration: configuration) webView.customUserAgent = "BaroSingo iOS/1.0 Turbo Native" return webView } Hotwire.loadPathConfiguration — 존재하지 않는 API // ❌ 컴파일 에러: no member 'loadPathConfiguration' Hotwire.loadPathConfiguration(from: [source]) // ✅ 해결: config.pathConfiguration.sources 직접 설정 Hotwire.config.pathConfiguration.sources = [ .file(Bundle.main.url(forResource: "path-configuration", withExtension: "json")!), .server(URL(string: "\(baseURL)/api/hotwire/path-configuration")!) ] Bridge Component에서 ViewController 접근 // ❌ 컴파일 에러: optional type must be unwrapped delegate.webView?.findViewController() // ✅ 해결: delegate?.destination 사용 guard let viewController = delegate?.destination as? UIViewController else { return } 교훈: Hotwire Native는 버전별 API 변경이 잦다. 공식 소스코드와 실제 동작하는 프로젝트를 참고하는 게 가장 확실하다. ...

2026-03-07 · 4분 소요 · Seunghan
개인정보처리방침 이용약관 면책조항 문의