👋 안녕하세요, 개발자 승한입니다.
모바일 앱과 웹 서비스를 만들고 있습니다. 사용자 경험을 개선하고 실생활 문제를 해결하는 것에 관심이 있습니다.
👋 안녕하세요, 개발자 승한입니다.
모바일 앱과 웹 서비스를 만들고 있습니다. 사용자 경험을 개선하고 실생활 문제를 해결하는 것에 관심이 있습니다.
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 변경이 잦다. 공식 소스코드와 실제 동작하는 프로젝트를 참고하는 게 가장 확실하다. ...

오늘 Rails 앱 배포가 build_failed로 떨어졌다. 처음엔 단순한 에러 하나겠지 싶었는데, 고칠 때마다 새 에러가 튀어나왔다. 결국 10개의 에러를 순서대로 해결하고 나서야 live 상태가 됐다. 연속 디버깅의 기록을 남긴다. 배경 Render에서 Rails 8 + Inertia.js + Svelte 5 조합 웹 서비스를 운영 중이다. 어느 날 대시보드를 보니 최신 배포가 build_failed 상태. 로그를 열었다. 에러 1: DB 연결 실패 — ActiveRecord::ConnectionNotEstablished bin/rails aborted! ActiveRecord::ConnectionNotEstablished: connection to server at "10.x.x.x", port 5432 failed: Connection refused Tasks: TOP => db:migrate 빌드 스크립트에서 db:migrate를 실행하는 순간 PostgreSQL 연결이 거부됐다. 트리거를 보니 service_resumed — 서비스가 재개(resume)된 것이었다. ...

Rails 8 + Inertia.js + Svelte 5 조합으로 만든 웹앱을 운영하다가, 기능은 돌아가는데 세부 UX가 들쭉날쭉하다는 걸 느꼈다. 이번 글은 전수 점검 후 우선순위 높은 4가지를 직접 고친 기록이다. 문제 발견: 같은 기능인데 UI가 다르다 가장 먼저 눈에 띈 건 시작일 입력 UI가 화면마다 다르게 동작하는 문제였다. 앱에는 할일을 만들 수 있는 진입점이 4곳이다. 대시보드 빠른 추가 모달(생성) 전체 페이지 생성 모달(수정) 위치 시작일 동작 대시보드 빠른추가 피커가 항상 노출 + + 시작일 버튼도 따로 존재 생성 모달 마감일 설정 후 + 시작일 추가 버튼 클릭 시 피커 표시 전체 페이지 피커 항상 노출 수정 모달 피커 항상 노출 생성 모달만 UX가 깔끔했고, 나머지 3곳은 피커가 항상 보여서 폼이 불필요하게 복잡해 보였다. + 시작일 버튼이 있는데 피커도 이미 떠 있으니 버튼의 의미가 모호했다. ...

Rails + Inertia.js + Svelte 5 기반으로 여러 프로젝트를 운영하다 보면 하나의 고질적 문제가 생긴다. 각 프로젝트마다 색상, 타이포그래피, 간격 등의 디자인 기준이 제각각이라는 점이다. 어떤 프로젝트는 tailwind.config.js에 체계적으로 정리되어 있고, 어떤 프로젝트는 bg-[#3182F6] 같은 하드코딩이 넘쳐난다. 이번에 전체 프로젝트를 대상으로 디자인 토큰 감사(audit)를 하고, 미적용 프로젝트에 체계를 잡은 과정을 기록한다. 1. 현황 감사: 8개 프로젝트 디자인 시스템 점검 먼저 모든 Svelte + Inertia.js 프로젝트를 대상으로 4가지 기준을 확인했다. 기준 확인 항목 UI 컴포넌트 components/ui/ 디렉토리 존재 여부와 컴포넌트 수 디자인 토큰 tokens.css 또는 CSS 변수 정의 파일 존재 테마 시스템 theme.ts, tailwind.config.js의 색상/타이포 확장 Storybook .storybook/ 디렉토리와 stories 파일 감사 결과 프로젝트 A ✅ 토큰 + Storybook + 카테고리별 컴포넌트 → 완전 적용 프로젝트 B ⚠️ 18개 UI + design-system 문서 있으나 토큰 파일 없음 프로젝트 C ⚠️ 22개 UI + theme.ts 있으나 토큰 체계 없음 프로젝트 D ❌ 15개 UI 있으나 토큰/테마 없음 (보일러플레이트) 프로젝트 E ❌ 5개 UI만, 토큰/테마 없음 프로젝트 F ❌ 도메인별 컴포넌트만, 공통 UI 없음 프로젝트 G ❌ 1개 UI만, 사실상 디자인 시스템 없음 프로젝트 H ❌ 프론트엔드 구조 자체 미완성 8개 중 완전 적용 1개, 부분 적용 2개, 미적용 5개. 예상보다 심각했다. ...

디자인 시스템이 어느 정도 잡힌 프로젝트에서 레퍼런스 앱을 받았을 때, “완전히 똑같이"가 아니라 “구성(composition)만 동일하게” 적용하고 싶었다. 이 글은 그 과정을 정리한 기록이다. 배경 기존 프로젝트에는 이미 다음이 갖춰져 있었다: 23개 공유 컴포넌트 (8개 카테고리: layout, navigation, input, overlay, card, data-display, social, feedback) CSS Custom Properties 기반 디자인 토큰 (colors, typography, spacing, radius, shadows, glassmorphism) Storybook 10 + Svelte 5 환경 (51개 story variants) 다크 테마 글래스모피즘 디자인 여기에 디자이너가 참조용으로 보내준 레퍼런스 앱 이미지를 분석해서, 기존 디자인을 깨지 않으면서 구조적 패턴만 흡수하는 작업을 진행했다. ...

Render에 올려둔 Rails 서비스 6개가 전부 각자 다른 에러를 토해내고 있었다. 하나씩 로그를 까보니 공통 패턴도 있고, 프로젝트마다 고유한 문제도 있었다. 한 세션에서 전부 수정하고 배포까지 마친 과정을 정리한다. 전체 상황 Render API로 서비스 6개의 로그를 일괄 조회했다. 결과: 서비스 주요 에러 서비스 A ERB 문법 에러로 500 (이미 커밋됐지만 미배포) 서비스 B Stoplight Light#run 블록 에러 + Telegram 파싱 에러 서비스 C solid_cache_entries 테이블 누락 서비스 D PG::UndefinedColumn + solid_cache 누락 서비스 E PG::DuplicateTable sessions + Sentry 초기화 에러 서비스 F TaskCleanupJob FK 위반 + Puma deprecated 경고 공통 패턴: Rails 8의 Solid Stack (SolidCache, SolidQueue, SolidCable) 초기 설정 문제가 여러 프로젝트에서 반복됐다. ...

두 개의 Rails 8 프로젝트를 병렬로 운영하다 보면 한쪽에서 공들여 만든 패턴이 다른 쪽에는 빠져있는 경우가 자주 생긴다. 기능을 구현할 때는 당장의 요구사항에 집중하다 보니 다른 프로젝트의 좋은 구현을 챙기지 못하는 것이다. 이번에 두 프로젝트를 나란히 놓고 비교하면서 빠진 부분을 서로 채워주는 작업을 했다. 주로 보안, PWA 경험, 에러 추적, 푸시 알림 인프라에 관한 내용이다. 비교 분석 방법 두 프로젝트의 주요 파일을 나열하고 대조했다. 확인 항목 ├── Gemfile (gem 목록) ├── config/initializers/ (설정 파일) ├── app/javascript/controllers/ (Stimulus 컨트롤러) ├── app/views/layouts/application.html.erb (레이아웃) ├── db/schema.rb (DB 스키마) └── ios/ (iOS 네이티브 설정) 결과적으로 아래 6가지를 양방향으로 이식했다. ...

두 개의 Rails 8 서비스가 있다. 하나는 메인 앱(IdP 역할), 다른 하나는 연동 서비스(RP 역할). 연동 서비스 로그인 페이지에 “메인 앱으로 로그인” 버튼을 넣고, SSO로 인증 후 돌아오는 플로우를 구현했다. 거기에 iOS Hotwire Native 앱이 설치돼 있으면, 브라우저 대신 네이티브 앱에서 인증이 진행되도록 Universal Links까지 붙였다. 목표 플로우 [연동 서비스] "메인 앱으로 로그인" 클릭 → 메인 앱 /auth/sso/authorize 로 리다이렉트 → (앱 설치 시) iOS Universal Link → 네이티브 앱 열림 → (미설치 시) 브라우저에서 로그인 → 이미 로그인 상태면 바로 토큰 발급 → 미로그인이면 OTP 로그인 → 토큰 발급 → "인증 완료" 페이지 (2초 대기) → 콜백 URL로 리다이렉트 → 연동 서비스가 토큰 검증 → 로그인 완료 삽질 1: SSO 파라미터가 로그인 과정에서 유실됨 문제 SSO authorize 엔드포인트에 before_action :require_authentication을 걸어놨더니: ...

Rails 앱 간 SSO(Single Sign-On)를 HMAC 기반으로 구현하던 중 예상치 못한 두 가지 버그를 만났다. 둘 다 Turbo Drive와 ERB의 동작 방식에서 비롯된 문제였다. 구현 개요 구조 IdP (Identity Provider): 사용자 인증을 담당하는 Rails 앱 (OTP 로그인) SP (Service Provider): IdP에서 인증받아 로그인하는 Rails 앱 플로우 SP 로그인 버튼 클릭 → SP: state 생성 후 세션 저장, IdP /authorize로 리다이렉트 → IdP: 로그인 확인 후 One-Time Token 발급 → IdP: authorize_complete 페이지 표시 (2초 후 SP callback으로 자동 리다이렉트) → SP callback: state 검증 + token 검증 → 로그인 완료 핵심 보안 요소 CSRF 방지: SP에서 생성한 state를 세션에 저장하고 callback에서 검증 HMAC 서명: SP가 IdP의 /verify 엔드포인트에 서명된 요청으로 token 검증 One-Time Token: 한 번 사용하면 무효화되는 토큰 버그 1: “state mismatch” — Turbo Drive prefetch가 세션을 덮어쓴다 증상 SP의 “SSO 로그인” 버튼을 클릭하면 IdP에서 인증 완료 페이지까지 잘 가는데, SP callback에서 항상 state mismatch 에러가 발생했다. ...

두 개의 Rails 앱이 있다. 하나는 내부 직원용 앱(OTP 로그인, 특정 도메인 전용), 다른 하나는 심사/관리 시스템으로 Devise + JWT 기반이다. 내부 직원이 심사 시스템에도 접근해야 하는데, 계정을 따로 만들어 관리하기 싫었다. “이미 내부 앱에 로그인돼 있으면, 심사 시스템에서 버튼 하나로 자동 로그인되면 안 되나?” OAuth2를 붙이면 정석이지만, Doorkeeper 설정하고 scope 관리하고… 내부 서비스 두 개 사이에 그게 과할 수 있다. 더 단순한 방법을 택했다. 구조 선택: One-Time Token + HMAC 이미 두 서비스 사이에 webhook 연동이 있었다. ITSM 이벤트를 다른 서비스에 전달할 때 HMAC-SHA256으로 서명하는 패턴이 있었고, 이걸 SSO에도 그대로 쓰기로 했다. ...